diff --git a/.env.example b/.env.example index 12753019..effe2d7b 100644 --- a/.env.example +++ b/.env.example @@ -6,8 +6,6 @@ AUTO_LINK_ALBY_ACCOUNT=false # Optionally set LDK debug log level to get more info #LDK_LOG_LEVEL=5 -# Optionally set Bark debug log level to get more info -#BARK_LOG_LEVEL=5 # Optionally set Main application debug log level to get more info #LOG_LEVEL=5 diff --git a/.github/workflows/http.yml b/.github/workflows/http.yml index 8353152d..9d01e13d 100644 --- a/.github/workflows/http.yml +++ b/.github/workflows/http.yml @@ -25,7 +25,6 @@ on: jobs: build: strategy: - fail-fast: false matrix: build: [ @@ -86,7 +85,7 @@ jobs: - name: Setup NodeJS uses: actions/setup-node@v4 with: - node-version: "22.x" + node-version: "20.x" - name: Run tests run: mkdir frontend/dist && touch frontend/dist/tmp && go test ./... @@ -129,15 +128,7 @@ jobs: run: go build ${{ env.GOTAGS }} -o build/bin/${{ env.PACKAGE_NAME }}/bin/${{ env.EXEC_NAME }} -ldflags "-X 'github.com/getAlby/hub/version.Tag=${{ env.TAG }}'" cmd/http/main.go - name: Import Code-Signing Certificates for macOS - if: >- - runner.os == 'macOS' && - ( - !github.event.pull_request || - ( - github.event.pull_request.head.repo.full_name == github.repository && - github.actor != 'dependabot[bot]' - ) - ) + if: runner.os == 'macOS' uses: Apple-Actions/import-codesign-certs@v3 with: # The certificates in a PKCS12 file encoded as a base64 string @@ -174,15 +165,7 @@ jobs: shell: bash - name: Sign the MacOS binary and libraries - if: >- - runner.os == 'macOS' && - ( - !github.event.pull_request || - ( - github.event.pull_request.head.repo.full_name == github.repository && - github.actor != 'dependabot[bot]' - ) - ) + if: runner.os == 'macOS' run: | /usr/bin/codesign -s "Developer ID Application: Alby Inc." -f -v --deep --timestamp --options runtime ./build/bin/${{ env.PACKAGE_NAME }}/bin/${{ env.EXEC_NAME }} /usr/bin/codesign -s "Developer ID Application: Alby Inc." -f -v --deep --timestamp --options runtime ./build/bin/${{ env.PACKAGE_NAME }}/lib/*.dylib @@ -206,16 +189,7 @@ jobs: cd ../../.. - name: Notarize the zip file - if: >- - runner.os == 'macOS' && - inputs.build-release && - ( - !github.event.pull_request || - ( - github.event.pull_request.head.repo.full_name == github.repository && - github.actor != 'dependabot[bot]' - ) - ) + if: runner.os == 'macOS' && inputs.build-release run: | echo "Notarizing Zip Files" gon -log-level=info -log-json ./build/darwin/http/gon-notarize.json diff --git a/.github/workflows/linting.yml b/.github/workflows/linting.yml index ee2e75c7..5bd519ad 100644 --- a/.github/workflows/linting.yml +++ b/.github/workflows/linting.yml @@ -18,7 +18,7 @@ jobs: - uses: actions/checkout@v3 - uses: actions/setup-node@v3 with: - node-version: 22.x + node-version: 20.x cache: "yarn" cache-dependency-path: frontend/yarn.lock diff --git a/.github/workflows/wails.yml b/.github/workflows/wails.yml index 0cf8acca..d4906852 100644 --- a/.github/workflows/wails.yml +++ b/.github/workflows/wails.yml @@ -73,10 +73,10 @@ jobs: - name: Setup NodeJS uses: actions/setup-node@v4 with: - node-version: "22.x" + node-version: "20.x" - name: Install Wails - run: go install github.com/wailsapp/wails/v2/cmd/wails@v2.14.0 + run: go install github.com/wailsapp/wails/v2/cmd/wails@v2.11.0 shell: bash - name: Install Linux Wails deps @@ -129,15 +129,7 @@ jobs: shell: bash - name: Import Code-Signing Certificates for macOS - if: >- - runner.os == 'macOS' && - ( - !github.event.pull_request || - ( - github.event.pull_request.head.repo.full_name == github.repository && - github.actor != 'dependabot[bot]' - ) - ) + if: runner.os == 'macOS' uses: Apple-Actions/import-codesign-certs@v3 with: # The certificates in a PKCS12 file encoded as a base64 string @@ -198,15 +190,7 @@ jobs: mv ./build/out/${{ env.PACKAGE_NAME }}.tar.bz2 ./build/bin/ - name: Sign the macOS binary - if: >- - runner.os == 'macOS' && - ( - !github.event.pull_request || - ( - github.event.pull_request.head.repo.full_name == github.repository && - github.actor != 'dependabot[bot]' - ) - ) + if: runner.os == 'macOS' run: | echo "Signing Package" /usr/bin/codesign -s "Developer ID Application: Alby Inc." -f -v --deep --timestamp --options runtime --entitlements ./build/darwin/entitlements.plist "./build/bin/${{ env.EXEC_NAME }}.app" @@ -238,15 +222,7 @@ jobs: EOF - name: Notarize the DMG image - if: >- - runner.os == 'macOS' && - ( - !github.event.pull_request || - ( - github.event.pull_request.head.repo.full_name == github.repository && - github.actor != 'dependabot[bot]' - ) - ) + if: runner.os == 'macOS' run: | echo "Notarizing Zip Files" gon -log-level=info -log-json ./build/darwin/gon-notarize.json diff --git a/.gitignore b/.gitignore index 7cfd951f..1b519bd7 100644 --- a/.gitignore +++ b/.gitignore @@ -30,5 +30,4 @@ glalby *.db-shm *.db-wal *.db-journal -albyhub-data -.claude/worktrees \ No newline at end of file +albyhub-data \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md index e99b0d33..b7d6e555 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -88,8 +88,6 @@ go run cmd/http/main.go wails dev -tags "wails" ``` -**Wails versions must stay in sync:** the `github.com/wailsapp/wails/v2` version in `go.mod` and the Wails CLI version installed in `.github/workflows/wails.yml` (`go install ...cmd/wails@vX.Y.Z`) must match. When bumping one, always update the other — this is a common source of drift (e.g. via Dependabot updates to `go.mod` only). - ## Testing ### Go Backend @@ -190,16 +188,6 @@ Code under `frontend/platform_specific/http/` and `frontend/platform_specific/wa - New screens added to `frontend/src/routes.tsx`. - ESLint + Prettier enforced via pre-commit hooks (husky). -### Branches - -Use a type prefix followed by a short, dash-separated summary: `feat/`, `chore/`, or `fix/`. For example: - -```text -feat/add-cashu-backend -fix/payment-timeout-crash -chore/bump-go-1.25 -``` - ### Commits Follow **Conventional Commits** format (`feat:`, `fix:`, `chore:`, etc.) — enforced by commitlint. diff --git a/Dockerfile b/Dockerfile index 68cfcdae..2023576a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM node:22-alpine AS frontend +FROM node:20-alpine AS frontend # Set the base path for the frontend build # This can be overridden at build time with --build-arg BASE_PATH= e.g. --build-arg BASE_PATH=/hub @@ -9,7 +9,7 @@ COPY frontend ./frontend RUN echo "Building frontend with base path $BASE_PATH" RUN cd frontend && yarn install --network-timeout 3000000 && yarn build:http -FROM golang:1.26.2 AS builder +FROM golang:1.25 AS builder ARG TARGETPLATFORM ARG BUILDPLATFORM diff --git a/README.md b/README.md index a80cdc04..5cbfc1b0 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ The application can run in two modes: Ideally the app runs 24/7 (on a node, VPS or always-online desktop/laptop machine) so it can be connected to a lightning address and receive online payments. -## Learn more about Alby Hub +## Run on Alby Cloud Visit [albyhub.com](https://albyhub.com) to learn more and get started and get Alby Hub running in minutes. @@ -207,12 +207,6 @@ Migration of the database is currently experimental. Please make a backup before go run cmd/db_migrate/main.go -from .data/nwc.db -to postgresql://myuser:mypass@localhost:5432/nwc -#### Migration from Postgres to Sqlite - -No manual steps are needed: create a migration file from Settings -> Migrate Alby Hub. The contents of the Postgres database will automatically be copied into a Sqlite database which is included in the migration file. Alternatively, run the migration tool manually: - -go run cmd/db_migrate/main.go -from postgresql://myuser:mypass@localhost:5432/nwc -to .data/nwc.db - ## Node-specific backend parameters - `ENABLE_ADVANCED_SETUP`: set to `false` to force a specific backend type (combined with backend parameters below) @@ -226,7 +220,6 @@ Can be configured via env or the UI - `CLN_LIGHTNING_DIR`: CLN's lightning directory containing the grpc certificates, usually `~/.lightning/` Optional for hold invoice methods support: - - `CLN_ADDRESS_HOLD`: the CLN hold plugin grpc address (grpc-host and grpc-port), e.g. `127.0.0.1:9738` If you are copying the certificates to another machine make sure you get the `ca.pem`, `client.pem` and `client-key.pem` from the lightning directory and optionally from the `hold` directory inside the lightning directory and keep the sub-directory structure of the hold directory. @@ -252,7 +245,6 @@ _To configure via env, the following parameters must be provided:_ - `LDK_MAX_PATH_COUNT`: Maximum number of paths that may be used by MPP payments. - `LDK_LOG_LEVEL`: Log level for the LDK node. Higher is more verbose. Default: 3. This is separate from the main application log level, allowing you to enable more verbose LDK logging (e.g., level 4, 5 or 6) without enabling verbose logging for the entire application. - `LDK_CHANNEL_MONITOR_WARNING_SIZE_BYTES`: If a channel monitor is larger than this value, a performance warning will be shown on the node page. -- `LDK_LSPS2_ADDRESSES`: Override the LSPS2 just-in-time (JIT) LSP provider for receiving. When set, Alby Hub can receive payments even without inbound liquidity: the configured LSP opens a channel on the fly and the fee is deducted from the incoming payment. Expected format is a single `@:`. When set, the "Open Your First Channel" prompts are hidden since the first channel is created automatically on the first receive. #### LDK Network Configuration @@ -291,16 +283,6 @@ _To configure via env, the following parameters must be provided:_ See [Phoenixd](scripts/linux-x86_64/phoenixd/README.md) -### Bark - -Bark connects to an [Ark](https://second.tech/) server. It can be configured via env. - -- `LN_BACKEND_TYPE`: BARK -- `BARK_SERVER`: the Ark server URL. For signet use `https://ark.signet.2nd.dev` -- `BARK_ESPLORA_SERVER`: the Esplora server URL used for chain data. For signet use `https://esplora.signet.2nd.dev`. -- `BARK_SERVER_ACCESS_TOKEN`: an optional access token, only required if using a private Ark server. -- `BARK_LOG_LEVEL`: Log level for Bark. Higher is more verbose. Default: 3. This is separate from the main application log level, allowing you to enable more verbose Bark logging (e.g., level 4 or 5) without enabling verbose logging for the entire application. - ### Alby OAuth Create an OAuth client at the [Alby Developer Portal](https://getalby.com/developer) and set your `ALBY_OAUTH_CLIENT_ID` and `ALBY_OAUTH_CLIENT_SECRET` in your .env. If not running locally, you'll also need to change your `BASE_URL`. @@ -402,8 +384,6 @@ Once the user has authorized the app connection a `nwc:success` message is sent If you need help contact support@getalby.com or reach out on Nostr: npub1getal6ykt05fsz5nqu4uld09nfj3y3qxmv8crys4aeut53unfvlqr80nfm You can also visit the chat of our Community on [Telegram](https://t.me/getalby). -For security vulnerabilities, please follow our [security policy](SECURITY.md). - ## ⚡️Donations Want to support the work on Alby? diff --git a/SECURITY.md b/SECURITY.md deleted file mode 100644 index 790ba2be..00000000 --- a/SECURITY.md +++ /dev/null @@ -1,7 +0,0 @@ -# Security Policy - -## Reporting a Vulnerability - -Please report suspected security vulnerabilities privately by emailing [security@getalby.com](mailto:security@getalby.com). Do not open a public issue or disclose the vulnerability publicly until we have coordinated a fix. - -Please include the affected version or component, the potential impact, and clear steps to reproduce the issue. We will acknowledge your report and keep you informed as we investigate and address it. diff --git a/alby/alby_oauth_service.go b/alby/alby_oauth_service.go index c496cc95..7c264d9b 100644 --- a/alby/alby_oauth_service.go +++ b/alby/alby_oauth_service.go @@ -408,7 +408,7 @@ func (svc *albyOAuthService) DeleteLightningAddress(ctx context.Context, address res, err := client.Do(req) if err != nil { - logger.Logger.WithError(err).Error("Failed to request delete lightning address endpoint") + logger.Logger.WithError(err).Error("Failed to delete lightning address endpoint") return err } @@ -418,13 +418,6 @@ func (svc *albyOAuthService) DeleteLightningAddress(ctx context.Context, address return errors.New("failed to read response body") } - if res.StatusCode == http.StatusNotFound { - // The lightning address was already deleted on the Alby account - // (e.g. removed previously). Treat as success so deletion is idempotent. - logger.Logger.WithField("address", address).Info("Lightning address already deleted on Alby account, ignoring 404") - return nil - } - if res.StatusCode >= 300 { return fmt.Errorf("DELETE request to /internal/lightning_addresses/%s returned non-success status: %d %s", address, res.StatusCode, string(responseBody)) } @@ -1376,52 +1369,6 @@ func (svc *albyOAuthService) requestAutoChannel(ctx context.Context, url string, }, nil } -func (svc *albyOAuthService) GetStories(ctx context.Context) ([]Story, error) { - client := &http.Client{Timeout: 10 * time.Second} - url := fmt.Sprintf("%s/stories", albyInternalAPIURL) - - req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) - if err != nil { - logger.Logger.WithError(err).Error("Error creating request to stories endpoint") - return nil, fmt.Errorf("create stories request: %w", err) - } - setDefaultRequestHeaders(req) - - res, err := client.Do(req) - if err != nil { - logger.Logger.WithError(err).Error("Failed to fetch stories from API") - return nil, fmt.Errorf("fetch stories: %w", err) - } - defer res.Body.Close() - - body, err := io.ReadAll(res.Body) - if err != nil { - logger.Logger.WithError(err).WithFields(logrus.Fields{ - "url": url, - }).Error("Failed to read response body") - return nil, fmt.Errorf("read stories response body: %w", err) - } - - if res.StatusCode >= 300 { - logger.Logger.WithFields(logrus.Fields{ - "body": string(body), - "status_code": res.StatusCode, - }).Error("stories endpoint returned non-success code") - return nil, fmt.Errorf("stories endpoint returned %d: %s", res.StatusCode, string(body)) - } - - var stories []Story - if err := json.Unmarshal(body, &stories); err != nil { - logger.Logger.WithFields(logrus.Fields{ - "body": string(body), - "error": err, - }).Error("Failed to decode stories API response") - return nil, fmt.Errorf("decode stories response: %w", err) - } - - return stories, nil -} - func setDefaultRequestHeaders(req *http.Request) { req.Header.Set("Content-Type", "application/json") req.Header.Set("User-Agent", "AlbyHub/"+version.Tag) @@ -1466,7 +1413,5 @@ func getEventWhitelist() []string { // client-side events "payment_failed_details", - "debit_card_url_clicked", - "debit_card_connect", } } diff --git a/alby/models.go b/alby/models.go index f51a636f..2fe5e6b3 100644 --- a/alby/models.go +++ b/alby/models.go @@ -32,7 +32,6 @@ type AlbyOAuthService interface { RemoveOAuthAccessToken() error CreateLightningAddress(ctx context.Context, address string, appId uint) (*CreateLightningAddressResponse, error) DeleteLightningAddress(ctx context.Context, address string) error - GetStories(ctx context.Context) ([]Story, error) } type CreateLightningAddressResponse struct { @@ -119,7 +118,6 @@ type ChannelPeerSuggestion struct { Description string `json:"description"` Note string `json:"note"` PublicChannelsAllowed bool `json:"publicChannelsAllowed"` - NodeAddress string `json:"nodeAddress"` FeeTotalSat1m *uint32 `json:"feeTotalSat1m"` FeeTotalSat2m *uint32 `json:"feeTotalSat2m"` FeeTotalSat3m *uint32 `json:"feeTotalSat3m"` @@ -155,20 +153,6 @@ type ErrorResponse struct { Message string `json:"message"` } -type StoryCta struct { - Label string `json:"label"` - URL string `json:"url"` - OpenInNewTab bool `json:"openInNewTab"` -} - -type Story struct { - ID int `json:"id"` - Title string `json:"title"` - Avatar string `json:"avatar"` - VideoID string `json:"videoId,omitempty"` - Cta *StoryCta `json:"cta,omitempty"` -} - type LSPChannelPaymentBolt11 struct { Invoice string `json:"invoice"` FeeTotalSat string `json:"fee_total_sat"` diff --git a/api/api.go b/api/api.go index fc53d8e0..f828a92a 100644 --- a/api/api.go +++ b/api/api.go @@ -2,11 +2,8 @@ package api import ( "context" - "crypto/tls" - "crypto/x509" "encoding/hex" "encoding/json" - "encoding/pem" "errors" "flag" "fmt" @@ -14,16 +11,13 @@ import ( "net/http" "net/url" "os" - "path/filepath" "slices" "strconv" "strings" "sync" - "sync/atomic" "time" "github.com/sirupsen/logrus" - "gopkg.in/macaroon.v2" "gorm.io/datatypes" "gorm.io/gorm" @@ -56,9 +50,6 @@ type api struct { startupError error startupErrorTime time.Time eventPublisher events.EventPublisher - // set after a migration file is created; the hub is halted at that point - // and the frontend should keep showing the migration success page - nodeMigrationFileCreated atomic.Bool } func NewAPI(svc service.Service, gormDB *gorm.DB, config config.Config, keys keys.Keys, albySvc alby.AlbyService, albyOAuthSvc alby.AlbyOAuthService, eventPublisher events.EventPublisher) *api { @@ -135,7 +126,21 @@ func (api *api) CreateApp(createAppRequest *CreateAppRequest) (*CreateAppRespons responseBody.RelayUrls = relayUrls responseBody.Lud16 = lightningAddress - responseBody.ReturnTo = buildReturnToUrl(createAppRequest.ReturnTo, relayUrls, *app.WalletPubkey, lightningAddress, app.Isolated) + if createAppRequest.ReturnTo != "" { + returnToUrl, err := url.Parse(createAppRequest.ReturnTo) + if err == nil { + query := returnToUrl.Query() + for _, relayUrl := range relayUrls { + query.Add("relay", relayUrl) + } + query.Add("pubkey", *app.WalletPubkey) + if lightningAddress != "" && !app.Isolated { + query.Add("lud16", lightningAddress) + } + returnToUrl.RawQuery = query.Encode() + responseBody.ReturnTo = returnToUrl.String() + } + } var lud16 string if lightningAddress != "" && !app.Isolated { @@ -146,28 +151,6 @@ func (api *api) CreateApp(createAppRequest *CreateAppRequest) (*CreateAppRespons return responseBody, nil } -// buildReturnToUrl adds the connection query parameters to the return_to -// URL the user will be redirected to. Only http and https URLs are accepted. -func buildReturnToUrl(returnTo string, relayUrls []string, walletPubkey string, lightningAddress string, isolated bool) string { - if returnTo == "" { - return "" - } - returnToUrl, err := url.Parse(returnTo) - if err != nil || (returnToUrl.Scheme != "http" && returnToUrl.Scheme != "https") { - return "" - } - query := returnToUrl.Query() - for _, relayUrl := range relayUrls { - query.Add("relay", relayUrl) - } - query.Add("pubkey", walletPubkey) - if lightningAddress != "" && !isolated { - query.Add("lud16", lightningAddress) - } - returnToUrl.RawQuery = query.Encode() - return returnToUrl.String() -} - func (api *api) UpdateApp(userApp *db.App, updateAppRequest *UpdateAppRequest) error { resolvedMaxAmountSat := ResolveToSat(updateAppRequest.MaxAmountSat, updateAppRequest.MaxAmountMsat, updateAppRequest.MaxAmount, nil) @@ -773,10 +756,6 @@ func (api *api) GetChannelPeerSuggestions(ctx context.Context) ([]alby.ChannelPe return api.albySvc.GetChannelPeerSuggestions(ctx) } -func (api *api) GetStories(ctx context.Context) ([]alby.Story, error) { - return api.albyOAuthSvc.GetStories(ctx) -} - func (api *api) GetLSPChannelOffer(ctx context.Context) (*alby.LSPChannelOffer, error) { return api.albyOAuthSvc.GetLSPChannelOffer(ctx) } @@ -855,20 +834,12 @@ func (api *api) Stop() error { return nil } -func (api *api) GetNodeConnectionInfo(ctx context.Context) (*NodeConnectionInfo, error) { +func (api *api) GetNodeConnectionInfo(ctx context.Context) (*lnclient.NodeConnectionInfo, error) { lnClient := api.svc.GetLNClient() if lnClient == nil { return nil, ErrLNClientNotStarted } - info, err := lnClient.GetNodeConnectionInfo(ctx) - if err != nil { - return nil, err - } - return &NodeConnectionInfo{ - Pubkey: info.Pubkey, - Address: info.Address, - Port: info.Port, - }, nil + return lnClient.GetNodeConnectionInfo(ctx) } func (api *api) RefundSwap(refundSwapRequest *RefundSwapRequest) error { @@ -1156,47 +1127,20 @@ func (api *api) GetSwapMnemonic() string { return api.keys.GetSwapMnemonic() } -func (api *api) GetNodeStatus(ctx context.Context) (*NodeStatus, error) { +func (api *api) GetNodeStatus(ctx context.Context) (*lnclient.NodeStatus, error) { lnClient := api.svc.GetLNClient() if lnClient == nil { return nil, ErrLNClientNotStarted } - nodeStatus, err := lnClient.GetNodeStatus(ctx) - if err != nil { - return nil, err - } - if nodeStatus == nil { - return nil, nil - } - return toApiNodeStatus(nodeStatus), nil + return lnClient.GetNodeStatus(ctx) } -func toApiNodeStatus(nodeStatus *lnclient.NodeStatus) *NodeStatus { - return &NodeStatus{ - IsReady: nodeStatus.IsReady, - InternalNodeStatus: nodeStatus.InternalNodeStatus, - } -} - -func (api *api) ListPeers(ctx context.Context) ([]PeerDetails, error) { +func (api *api) ListPeers(ctx context.Context) ([]lnclient.PeerDetails, error) { lnClient := api.svc.GetLNClient() if lnClient == nil { return nil, ErrLNClientNotStarted } - peers, err := lnClient.ListPeers(ctx) - if err != nil { - return nil, err - } - apiPeers := make([]PeerDetails, 0, len(peers)) - for _, peer := range peers { - apiPeers = append(apiPeers, PeerDetails{ - NodeId: peer.NodeId, - Address: peer.Address, - IsPersisted: peer.IsPersisted, - IsConnected: peer.IsConnected, - }) - } - return apiPeers, nil + return lnClient.ListPeers(ctx) } func (api *api) ConnectPeer(ctx context.Context, connectPeerRequest *ConnectPeerRequest) error { @@ -1204,11 +1148,7 @@ func (api *api) ConnectPeer(ctx context.Context, connectPeerRequest *ConnectPeer if lnClient == nil { return ErrLNClientNotStarted } - return lnClient.ConnectPeer(ctx, &lnclient.ConnectPeerRequest{ - Pubkey: connectPeerRequest.Pubkey, - Address: connectPeerRequest.Address, - Port: connectPeerRequest.Port, - }) + return lnClient.ConnectPeer(ctx, connectPeerRequest) } func (api *api) OpenChannel(ctx context.Context, openChannelRequest *OpenChannelRequest) (*OpenChannelResponse, error) { @@ -1216,17 +1156,7 @@ func (api *api) OpenChannel(ctx context.Context, openChannelRequest *OpenChannel if lnClient == nil { return nil, ErrLNClientNotStarted } - resp, err := lnClient.OpenChannel(ctx, &lnclient.OpenChannelRequest{ - Pubkey: openChannelRequest.Pubkey, - AmountSats: openChannelRequest.AmountSats, - Public: openChannelRequest.Public, - }) - if err != nil { - return nil, err - } - return &OpenChannelResponse{ - FundingTxId: resp.FundingTxId, - }, nil + return lnClient.OpenChannel(ctx, openChannelRequest) } func (api *api) DisconnectPeer(ctx context.Context, peerId string) error { @@ -1250,15 +1180,11 @@ func (api *api) CloseChannel(ctx context.Context, peerId, channelId string, forc "channel_id": channelId, "force": force, }).Info("Closing channel") - err := lnClient.CloseChannel(ctx, &lnclient.CloseChannelRequest{ + return lnClient.CloseChannel(ctx, &lnclient.CloseChannelRequest{ NodeId: peerId, ChannelId: channelId, Force: force, }) - if err != nil { - return nil, err - } - return &CloseChannelResponse{}, nil } func (api *api) UpdateChannel(ctx context.Context, updateChannelRequest *UpdateChannelRequest) error { @@ -1269,13 +1195,7 @@ func (api *api) UpdateChannel(ctx context.Context, updateChannelRequest *UpdateC logger.Logger.WithFields(logrus.Fields{ "request": updateChannelRequest, }).Info("updating channel") - return lnClient.UpdateChannel(ctx, &lnclient.UpdateChannelRequest{ - ChannelId: updateChannelRequest.ChannelId, - NodeId: updateChannelRequest.NodeId, - ForwardingFeeBaseMsat: updateChannelRequest.ForwardingFeeBaseMsat, - ForwardingFeeProportionalMillionths: updateChannelRequest.ForwardingFeeProportionalMillionths, - MaxDustHtlcExposureFromFeeRateMultiplier: updateChannelRequest.MaxDustHtlcExposureFromFeeRateMultiplier, - }) + return lnClient.UpdateChannel(ctx, updateChannelRequest) } func (api *api) MakeOffer(ctx context.Context, description string) (string, error) { @@ -1386,70 +1306,7 @@ func (api *api) GetBalances(ctx context.Context) (*BalancesResponse, error) { if err != nil { return nil, err } - return toApiBalances(balances), nil -} - -func toApiBalances(balances *lnclient.BalancesResponse) *BalancesResponse { - totalSpendableMsat := balances.Lightning.TotalSpendableMsat - totalReceivableMsat := balances.Lightning.TotalReceivableMsat - nextMaxSpendableMsat := balances.Lightning.NextMaxSpendableMsat - nextMaxReceivableMsat := balances.Lightning.NextMaxReceivableMsat - nextMaxSpendableMPPMsat := balances.Lightning.NextMaxSpendableMPPMsat - nextMaxReceivableMPPMsat := balances.Lightning.NextMaxReceivableMPPMsat - - return &BalancesResponse{ - Onchain: OnchainBalanceResponse{ - Spendable: balances.Onchain.SpendableSat, - SpendableSat: balances.Onchain.SpendableSat, - Total: balances.Onchain.TotalSat, - TotalSat: balances.Onchain.TotalSat, - Reserved: balances.Onchain.ReservedSat, - ReservedSat: balances.Onchain.ReservedSat, - PendingBalancesFromChannelClosures: balances.Onchain.PendingBalancesFromChannelClosuresSat, - PendingBalancesFromChannelClosuresSat: balances.Onchain.PendingBalancesFromChannelClosuresSat, - PendingBalancesDetails: toApiPendingBalanceDetails(balances.Onchain.PendingBalancesDetails), - PendingSweepBalancesDetails: toApiPendingBalanceDetails(balances.Onchain.PendingSweepBalancesDetails), - InternalBalances: balances.Onchain.InternalBalances, - }, - Lightning: LightningBalanceResponse{ - TotalSpendable: totalSpendableMsat, - TotalSpendableSat: totalSpendableMsat / 1000, - TotalSpendableMsat: totalSpendableMsat, - TotalReceivable: totalReceivableMsat, - TotalReceivableSat: totalReceivableMsat / 1000, - TotalReceivableMsat: totalReceivableMsat, - NextMaxSpendable: nextMaxSpendableMsat, - NextMaxSpendableSat: nextMaxSpendableMsat / 1000, - NextMaxSpendableMsat: nextMaxSpendableMsat, - NextMaxReceivable: nextMaxReceivableMsat, - NextMaxReceivableSat: nextMaxReceivableMsat / 1000, - NextMaxReceivableMsat: nextMaxReceivableMsat, - NextMaxSpendableMPP: nextMaxSpendableMPPMsat, - NextMaxSpendableMPPSat: nextMaxSpendableMPPMsat / 1000, - NextMaxSpendableMPPMsat: nextMaxSpendableMPPMsat, - NextMaxReceivableMPP: nextMaxReceivableMPPMsat, - NextMaxReceivableMPPSat: nextMaxReceivableMPPMsat / 1000, - NextMaxReceivableMPPMsat: nextMaxReceivableMPPMsat, - }, - } -} - -func toApiPendingBalanceDetails(details []lnclient.PendingBalanceDetails) []PendingBalanceDetails { - if details == nil { - return nil - } - apiDetails := make([]PendingBalanceDetails, 0, len(details)) - for _, d := range details { - apiDetails = append(apiDetails, PendingBalanceDetails{ - ChannelId: d.ChannelId, - NodeId: d.NodeId, - Amount: d.AmountSat, - AmountSat: d.AmountSat, - FundingTxId: d.FundingTxId, - FundingTxVout: d.FundingTxVout, - }) - } - return apiDetails + return balances, nil } // TODO: remove dependency on this endpoint @@ -1508,22 +1365,8 @@ func (api *api) RequestMempoolApi(ctx context.Context, endpoint string) (interfa func (api *api) GetInfo(ctx context.Context) (*InfoResponse, error) { info := InfoResponse{} - - if api.nodeMigrationFileCreated.Load() { - // the hub is halted and the database is closed after a migration file - // is created, so return a minimal response without reading any config - // or node state; the frontend only needs the flag to keep showing the - // migration success page - info.NodeMigrationFileCreated = true - info.SetupCompleted = true - info.Version = version.Tag - info.Relays = []InfoResponseRelay{} - return &info, nil - } - backendType, _ := api.cfg.Get("LNBackendType", "") ldkVssEnabled, _ := api.cfg.Get("LdkVssEnabled", "") - jitChannelsEnabled, _ := api.cfg.Get("JitChannelsEnabled", "") autoUnlockPassword, _ := api.cfg.Get("AutoUnlockPassword", "") setupCompleted, err := api.cfg.SetupCompleted() if err != nil { @@ -1540,7 +1383,6 @@ func (api *api) GetInfo(ctx context.Context) (*InfoResponse, error) { } lnClient := api.svc.GetLNClient() info.Running = lnClient != nil - info.NodeMigrationFileCreated = api.nodeMigrationFileCreated.Load() info.BackendType = backendType info.AlbyAuthUrl = api.albyOAuthSvc.GetAuthUrl() info.OAuthRedirect = !api.cfg.GetEnv().IsDefaultClientId() @@ -1548,11 +1390,7 @@ func (api *api) GetInfo(ctx context.Context) (*InfoResponse, error) { info.EnableAdvancedSetup = api.cfg.GetEnv().EnableAdvancedSetup info.HideUpdateBanner = api.cfg.GetEnv().HideUpdateBanner info.LdkVssEnabled = ldkVssEnabled == "true" - info.JitChannelsEnabled = jitChannelsEnabled != "false" info.VssSupported = backendType == config.LDKBackendType && api.cfg.GetEnv().LDKVssUrl != "" - info.LdkVssUrl = api.cfg.GetEnv().LDKVssUrl - info.DatabaseType = api.db.Dialector.Name() - info.SupportsBolt12 = backendType == config.LDKBackendType || backendType == config.CLNBackendType info.AutoUnlockPasswordEnabled = autoUnlockPassword != "" info.AutoUnlockPasswordSupported = api.cfg.GetEnv().IsDefaultClientId() info.Relays = []InfoResponseRelay{} @@ -1587,28 +1425,10 @@ func (api *api) GetInfo(ctx context.Context) (*InfoResponse, error) { type chainSourceProvider interface { GetChainDataSource() (string, string) } - type lsps2SourceProvider interface { - GetLiquiditySourceLsps2() string - } - type lsps2MinPaymentSizeProvider interface { - GetLiquiditySourceLsps2MinPaymentSizeMsat() *uint64 - } - type lsps2MaxPaymentSizeProvider interface { - GetLiquiditySourceLsps2MaxPaymentSizeMsat() *uint64 - } if ldkService, ok := api.svc.GetLNClient().(chainSourceProvider); ok { info.ChainDataSourceType, info.ChainDataSourceAddress = ldkService.GetChainDataSource() } - if ldkService, ok := api.svc.GetLNClient().(lsps2SourceProvider); ok { - info.JitChannelsLiquiditySource = ldkService.GetLiquiditySourceLsps2() - } - if ldkService, ok := api.svc.GetLNClient().(lsps2MinPaymentSizeProvider); ok { - info.JitChannelsMinPaymentSizeMsat = ldkService.GetLiquiditySourceLsps2MinPaymentSizeMsat() - } - if ldkService, ok := api.svc.GetLNClient().(lsps2MaxPaymentSizeProvider); ok { - info.JitChannelsMaxPaymentSizeMsat = ldkService.GetLiquiditySourceLsps2MaxPaymentSizeMsat() - } } } @@ -1619,7 +1439,7 @@ func (api *api) GetInfo(ctx context.Context) (*InfoResponse, error) { return &info, nil } -func (api *api) setCurrency(currency string) error { +func (api *api) SetCurrency(currency string) error { if currency == "" { return fmt.Errorf("currency value cannot be empty") } @@ -1633,7 +1453,7 @@ func (api *api) setCurrency(currency string) error { return nil } -func (api *api) setBitcoinDisplayFormat(format string) error { +func (api *api) SetBitcoinDisplayFormat(format string) error { if format != constants.BITCOIN_DISPLAY_FORMAT_SATS && format != constants.BITCOIN_DISPLAY_FORMAT_BIP177 { return fmt.Errorf("bitcoin display format must be '%s' or '%s'", constants.BITCOIN_DISPLAY_FORMAT_SATS, constants.BITCOIN_DISPLAY_FORMAT_BIP177) } @@ -1647,43 +1467,21 @@ func (api *api) setBitcoinDisplayFormat(format string) error { return nil } -func (api *api) setJitChannelsEnabled(enabled bool) error { - value := "true" - if !enabled { - value = "false" - } - - err := api.cfg.SetUpdate("JitChannelsEnabled", value, "") - if err != nil { - logger.Logger.WithError(err).Error("Failed to update JIT channels setting") - return err - } - - return nil -} - func (api *api) UpdateSettings(updateSettingsRequest *UpdateSettingsRequest) error { if updateSettingsRequest.Currency != "" { - err := api.setCurrency(updateSettingsRequest.Currency) + err := api.SetCurrency(updateSettingsRequest.Currency) if err != nil { return fmt.Errorf("failed to set currency: %w", err) } } if updateSettingsRequest.BitcoinDisplayFormat != "" { - err := api.setBitcoinDisplayFormat(updateSettingsRequest.BitcoinDisplayFormat) + err := api.SetBitcoinDisplayFormat(updateSettingsRequest.BitcoinDisplayFormat) if err != nil { return fmt.Errorf("failed to set bitcoin display format: %w", err) } } - if updateSettingsRequest.JitChannelsEnabled != nil { - err := api.setJitChannelsEnabled(*updateSettingsRequest.JitChannelsEnabled) - if err != nil { - return fmt.Errorf("failed to set JIT channels setting: %w", err) - } - } - return nil } @@ -1763,19 +1561,6 @@ func (api *api) Setup(ctx context.Context, setupRequest *SetupRequest) error { return errors.New("no unlock password provided") } - // Bark and Cashu both store wallet state on local disk, so they cannot - // run in environments without persistent volumes (e.g. Alby Cloud). Bark - // can recover spendable VTXOs from the mnemonic alone, but in-flight - // payment checkpoints and wallet metadata are local-only, so persistent - // storage is still required. The default OAuth client ID identifies a - // local / self-hosted deployment. - if !api.cfg.GetEnv().IsDefaultClientId() { - switch setupRequest.LNBackendType { - case config.BarkBackendType, config.CashuBackendType: - return fmt.Errorf("%s backend is not supported in this environment (no persistent storage)", setupRequest.LNBackendType) - } - } - err = api.cfg.SaveUnlockPasswordCheck(setupRequest.UnlockPassword) if err != nil { return err @@ -1810,18 +1595,12 @@ func (api *api) Setup(ctx context.Context, setupRequest *SetupRequest) error { } } if setupRequest.LNDCertFile != "" { - // The file path is provided by the (unauthenticated) setup request, so - // only persist the content if it parses as a certificate. Storing the - // re-encoded certificate(s) guarantees nothing but the parsed structure - // reaches the database - e.g. a private key bundled in the same PEM file - // is dropped rather than persisted. - certHex, err := readAndCanonicalizeLNDCert(setupRequest.LNDCertFile) + certBytes, err := os.ReadFile(setupRequest.LNDCertFile) if err != nil { - // Return a generic error and log the detail server-side so the - // response is not a file existence/readability oracle. - logger.Logger.WithError(err).Error("Failed to process lnd cert file") - return errors.New("invalid LND certificate file") + logger.Logger.WithError(err).Error("Failed to read lnd cert file") + return err } + certHex := hex.EncodeToString(certBytes) err = api.cfg.SetUpdate("LNDCertHex", certHex, setupRequest.UnlockPassword) if err != nil { logger.Logger.WithError(err).Error("Failed to save lnd cert hex") @@ -1829,17 +1608,12 @@ func (api *api) Setup(ctx context.Context, setupRequest *SetupRequest) error { } } if setupRequest.LNDMacaroonFile != "" { - // The file path is provided by the (unauthenticated) setup request, so - // only persist the content if it parses as a macaroon. Storing the - // re-marshalled macaroon guarantees only the parsed structure reaches - // the database. - macaroonHex, err := readAndCanonicalizeLNDMacaroon(setupRequest.LNDMacaroonFile) + macaroonBytes, err := os.ReadFile(setupRequest.LNDMacaroonFile) if err != nil { - // Return a generic error and log the detail server-side so the - // response is not a file existence/readability oracle. - logger.Logger.WithError(err).Error("Failed to process lnd macaroon file") - return errors.New("invalid LND macaroon file") + logger.Logger.WithError(err).Error("Failed to read lnd macaroon file") + return err } + macaroonHex := hex.EncodeToString(macaroonBytes) err = api.cfg.SetUpdate("LNDMacaroonHex", macaroonHex, setupRequest.UnlockPassword) if err != nil { logger.Logger.WithError(err).Error("Failed to save lnd macaroon hex") @@ -1879,15 +1653,6 @@ func (api *api) Setup(ctx context.Context, setupRequest *SetupRequest) error { } if setupRequest.CLNLightningDir != "" { - // The directory path is provided by the (unauthenticated) setup request. - // Validate that it holds the expected CLN TLS credentials before saving, - // so the path cannot be used as an existence/readability oracle for - // arbitrary directories (the failure otherwise surfaces via startupError - // on the anonymous /api/info response). - if err := validateCLNLightningDir(setupRequest.CLNLightningDir, setupRequest.CLNAddressHold != ""); err != nil { - logger.Logger.WithError(err).Error("Failed to validate CLN lightning directory") - return errors.New("invalid CLN lightning directory") - } err = api.cfg.SetUpdate("CLNLightningDir", setupRequest.CLNLightningDir, setupRequest.UnlockPassword) if err != nil { logger.Logger.WithError(err).Error("Failed to save CLN Lightning directory path") @@ -1906,101 +1671,6 @@ func (api *api) Setup(ctx context.Context, setupRequest *SetupRequest) error { return nil } -// readAndCanonicalizeLNDCert reads the LND TLS certificate at the given path, -// validates that it contains at least one parseable certificate, and returns -// the hex-encoded re-encoding of only the parsed certificate(s). Any non -// CERTIFICATE PEM blocks (e.g. a bundled private key) are discarded so they are -// never persisted. Callers must not reflect the returned error to the client. -func readAndCanonicalizeLNDCert(path string) (string, error) { - raw, err := os.ReadFile(path) - if err != nil { - return "", fmt.Errorf("failed to read LND cert file: %w", err) - } - - var canonical []byte - rest := raw - for { - var block *pem.Block - block, rest = pem.Decode(rest) - if block == nil { - break - } - if block.Type != "CERTIFICATE" { - continue - } - cert, err := x509.ParseCertificate(block.Bytes) - if err != nil { - return "", fmt.Errorf("failed to parse LND certificate: %w", err) - } - canonical = append(canonical, pem.EncodeToMemory(&pem.Block{ - Type: "CERTIFICATE", - Bytes: cert.Raw, - })...) - } - if len(canonical) == 0 { - return "", errors.New("no valid certificate found in LND cert file") - } - - return hex.EncodeToString(canonical), nil -} - -// readAndCanonicalizeLNDMacaroon reads the LND macaroon at the given path, -// validates that it is a well-formed macaroon, and returns the hex-encoded -// re-marshalling so that only the parsed structure is persisted. Callers must -// not reflect the returned error to the client. -func readAndCanonicalizeLNDMacaroon(path string) (string, error) { - raw, err := os.ReadFile(path) - if err != nil { - return "", fmt.Errorf("failed to read LND macaroon file: %w", err) - } - - mac := &macaroon.Macaroon{} - if err := mac.UnmarshalBinary(raw); err != nil { - return "", fmt.Errorf("failed to parse LND macaroon: %w", err) - } - canonical, err := mac.MarshalBinary() - if err != nil { - return "", fmt.Errorf("failed to marshal LND macaroon: %w", err) - } - - return hex.EncodeToString(canonical), nil -} - -// validateCLNLightningDir checks that the given directory holds the CLN TLS -// credentials that will later be loaded at connect time (ca.pem, client.pem, -// client-key.pem), for each gRPC server name the config will use. This mirrors -// the parses performed by the CLN client's loadTLSCredentials so a directory -// that passes here is one CLN can actually use. Callers must not reflect the -// returned error to the client. -func validateCLNLightningDir(lightningDir string, hold bool) error { - // "cln" reads the directory directly; other server names are joined as a - // subdirectory, matching loadTLSCredentials in lnclient/cln. - serverNames := []string{"cln"} - if hold { - serverNames = append(serverNames, "hold") - } - - for _, serverName := range serverNames { - dir := lightningDir - if serverName != "cln" { - dir = filepath.Join(dir, serverName) - } - - caPEM, err := os.ReadFile(filepath.Join(dir, "ca.pem")) - if err != nil { - return fmt.Errorf("failed to read CLN CA cert (%s): %w", serverName, err) - } - if !x509.NewCertPool().AppendCertsFromPEM(caPEM) { - return fmt.Errorf("failed to parse CLN CA cert (%s)", serverName) - } - if _, err := tls.LoadX509KeyPair(filepath.Join(dir, "client.pem"), filepath.Join(dir, "client-key.pem")); err != nil { - return fmt.Errorf("failed to load CLN client cert/key (%s): %w", serverName, err) - } - } - - return nil -} - func (api *api) GetWalletCapabilities(ctx context.Context) (*WalletCapabilitiesResponse, error) { lnClient := api.svc.GetLNClient() if lnClient == nil { @@ -2067,27 +1737,12 @@ func (api *api) SyncWallet() error { lnClient.UpdateLastWalletSyncRequest() return nil } -func (api *api) ListOnchainTransactions(ctx context.Context) ([]OnchainTransaction, error) { +func (api *api) ListOnchainTransactions(ctx context.Context) ([]lnclient.OnchainTransaction, error) { lnClient := api.svc.GetLNClient() if lnClient == nil { return nil, ErrLNClientNotStarted } - transactions, err := lnClient.ListOnchainTransactions(ctx) - if err != nil { - return nil, err - } - apiTransactions := make([]OnchainTransaction, 0, len(transactions)) - for _, t := range transactions { - apiTransactions = append(apiTransactions, OnchainTransaction{ - AmountSat: t.AmountSat, - CreatedAt: t.CreatedAt, - State: t.State, - Type: t.Type, - NumConfirmations: t.NumConfirmations, - TxId: t.TxId, - }) - } - return apiTransactions, nil + return lnClient.ListOnchainTransactions(ctx) } func (api *api) GetLogOutput(ctx context.Context, logType string, getLogRequest *GetLogOutputRequest) (*GetLogOutputResponse, error) { @@ -2163,11 +1818,7 @@ func (api *api) Health(ctx context.Context) (*HealthResponse, error) { if lnClient != nil { nodeStatus, _ := lnClient.GetNodeStatus(ctx) if nodeStatus == nil || !nodeStatus.IsReady { - var apiNodeStatus *NodeStatus - if nodeStatus != nil { - apiNodeStatus = toApiNodeStatus(nodeStatus) - } - alarms = append(alarms, NewHealthAlarm(HealthAlarmKindNodeNotReady, apiNodeStatus)) + alarms = append(alarms, NewHealthAlarm(HealthAlarmKindNodeNotReady, nodeStatus)) } channels, err := lnClient.ListChannels(ctx) diff --git a/api/apps_test.go b/api/apps_test.go index 6cd69daa..ff00783f 100644 --- a/api/apps_test.go +++ b/api/apps_test.go @@ -9,32 +9,6 @@ import ( "github.com/stretchr/testify/require" ) -func TestBuildReturnToUrl(t *testing.T) { - relayUrls := []string{"wss://relay.getalby.com/v1"} - walletPubkey := "6f8bf1b7d58ac41b2c793837ba528c1d0a4a1cd2e3f5b7c9d0e1f2a3b4c5d6e7" - - assert.Equal(t, - "https://example.com?pubkey=6f8bf1b7d58ac41b2c793837ba528c1d0a4a1cd2e3f5b7c9d0e1f2a3b4c5d6e7&relay=wss%3A%2F%2Frelay.getalby.com%2Fv1", - buildReturnToUrl("https://example.com", relayUrls, walletPubkey, "", false)) - - // existing query parameters are preserved and lud16 is added - assert.Equal(t, - "https://example.com/path?foo=bar&lud16=user%40getalby.com&pubkey=6f8bf1b7d58ac41b2c793837ba528c1d0a4a1cd2e3f5b7c9d0e1f2a3b4c5d6e7&relay=wss%3A%2F%2Frelay.getalby.com%2Fv1", - buildReturnToUrl("https://example.com/path?foo=bar", relayUrls, walletPubkey, "user@getalby.com", false)) - - // isolated apps do not receive a lightning address - assert.Equal(t, - "http://example.com?pubkey=6f8bf1b7d58ac41b2c793837ba528c1d0a4a1cd2e3f5b7c9d0e1f2a3b4c5d6e7&relay=wss%3A%2F%2Frelay.getalby.com%2Fv1", - buildReturnToUrl("http://example.com", relayUrls, walletPubkey, "user@getalby.com", true)) - - // only http and https URLs are accepted - assert.Equal(t, "", buildReturnToUrl("", relayUrls, walletPubkey, "", false)) - assert.Equal(t, "", buildReturnToUrl("example.com/path", relayUrls, walletPubkey, "", false)) - assert.Equal(t, "", buildReturnToUrl("example://app", relayUrls, walletPubkey, "", false)) - assert.Equal(t, "", buildReturnToUrl("javascript:void(0)", relayUrls, walletPubkey, "", false)) - assert.Equal(t, "", buildReturnToUrl("::invalid::", relayUrls, walletPubkey, "", false)) -} - func TestCreateApp_SuperuserScopeIncorrectPassword(t *testing.T) { cfg := mocks.NewMockConfig(t) cfg.On("CheckUnlockPassword", "").Return(false) diff --git a/api/backup.go b/api/backup.go index 32e5aa03..9d89ef7b 100644 --- a/api/backup.go +++ b/api/backup.go @@ -1,11 +1,9 @@ package api import ( - "bytes" "errors" "fmt" "io" - "math" "strings" "time" @@ -18,48 +16,12 @@ import ( "crypto/rand" "crypto/sha256" - "github.com/getAlby/hub/config" "github.com/getAlby/hub/db" "github.com/getAlby/hub/logger" "github.com/getAlby/hub/utils" "golang.org/x/crypto/pbkdf2" ) -// zipMagic is the ZIP local file header signature "PK\x03\x04" — the first -// four bytes of every ZIP file, and therefore of every archive produced by -// CreateBackup. decryptingReader uses it to detect which cipher scheme the -// backup file was created with. -var zipMagic = []byte{'P', 'K', 0x03, 0x04} - -// backupCipher describes one of the cipher schemes used for backup files, -// which are laid out as salt || iv || encrypted zip archive. -type backupCipher struct { - saltSize int - deriveKey func(password string, salt []byte) ([]byte, error) - newStream func(block cipher.Block, iv []byte) cipher.Stream -} - -var backupCiphers = []backupCipher{ - // current scheme, used for all new backup files - { - saltSize: 32, - deriveKey: func(password string, salt []byte) ([]byte, error) { - key, _, err := config.DeriveKey(password, salt) - return key, err - }, - newStream: cipher.NewCTR, - }, - // legacy scheme, kept to restore backup files created by older versions - { - saltSize: 8, - deriveKey: func(password string, salt []byte) ([]byte, error) { - return pbkdf2.Key([]byte(password), salt, 4096, 32, sha256.New), nil - }, - //nolint:staticcheck // OFB is required to read files created by older versions - newStream: cipher.NewOFB, - }, -} - func (api *api) CreateBackup(unlockPassword string, w io.Writer) error { logger.Logger.Info("Creating backup to migrate Alby Hub to another device") var err error @@ -76,9 +38,8 @@ func (api *api) CreateBackup(unlockPassword string, w io.Writer) error { return errors.New("Please disable auto-unlock before using this feature") } - dbBackend := api.db.Dialector.Name() - if dbBackend != "sqlite" && dbBackend != "postgres" { - return fmt.Errorf("migration with %s backend is currently not supported", dbBackend) + if api.db.Dialector.Name() != "sqlite" { + return errors.New("Migration with non-sqlite backend is currently not supported") } workDir, err := filepath.Abs(api.cfg.GetEnv().Workdir) @@ -115,50 +76,6 @@ func (api *api) CreateBackup(unlockPassword string, w io.Writer) error { return errors.New("failed to remove oauth access token") } - // Locate the main database file. - dbFilePath := api.cfg.GetEnv().DatabaseUri - - if dbBackend == "postgres" { - // The migration file must contain a sqlite database, so copy the - // contents of the postgres database into a temporary sqlite database - // and add that to the archive instead. - dbFilePath = filepath.Join(workDir, "migration.db") - - removeConvertedDb := func() { - for _, path := range []string{dbFilePath, dbFilePath + "-wal", dbFilePath + "-shm"} { - if err := os.Remove(path); err != nil && !errors.Is(err, os.ErrNotExist) { - logger.Logger.WithError(err).WithField("path", path).Error("Failed to remove converted database file") - } - } - } - // Remove stale files from a previously failed migration attempt. - removeConvertedDb() - defer removeConvertedDb() - - logger.Logger.WithField("path", dbFilePath).Info("Copying postgres database to sqlite") - sqliteDb, err := db.NewDB(dbFilePath, api.cfg.GetEnv().LogDBQueries) - if err != nil { - logger.Logger.WithError(err).Error("Failed to create sqlite database for migration") - return fmt.Errorf("failed to create sqlite database for migration: %w", err) - } - - err = db.MigrateDB(api.db, sqliteDb) - if err != nil { - logger.Logger.WithError(err).Error("Failed to copy database contents to sqlite") - if stopErr := db.Stop(sqliteDb); stopErr != nil { - logger.Logger.WithError(stopErr).Error("Failed to stop sqlite database") - } - return fmt.Errorf("failed to copy database contents to sqlite: %w", err) - } - - // Close the sqlite database to checkpoint the WAL before archiving it. - err = db.Stop(sqliteDb) - if err != nil { - logger.Logger.WithError(err).Error("Failed to stop sqlite database") - return fmt.Errorf("failed to close sqlite database: %w", err) - } - } - // Closing the database leaves the service in an inconsistent state, // but that should not be a problem since the app is not expected // to be used after its data is exported. @@ -209,6 +126,8 @@ func (api *api) CreateBackup(unlockPassword string, w io.Writer) error { return err } + // Locate the main database file. + dbFilePath := api.cfg.GetEnv().DatabaseUri // Add the database file to the archive. logger.Logger.WithField("nwc.db", dbFilePath).Info("adding nwc db to zip") err = addFileToZip(dbFilePath, "nwc.db") @@ -233,18 +152,8 @@ func (api *api) CreateBackup(unlockPassword string, w io.Writer) error { } } - // Finalize the archive before reporting success; the deferred close - // only covers early returns. - err = zw.Close() - if err != nil { - logger.Logger.WithError(err).Error("Failed to finalize migration archive") - return fmt.Errorf("failed to finalize migration archive: %w", err) - } - logger.Logger.Info("Successfully created backup to migrate Alby Hub to another device") - api.nodeMigrationFileCreated.Store(true) - return nil } @@ -295,38 +204,8 @@ func (api *api) RestoreBackup(unlockPassword string, r io.Reader) error { return fmt.Errorf("failed to create zip reader: %w", err) } - if len(zr.File) == 0 { - return errors.New("backup file contains no files") - } - - restoreDir := filepath.Join(workDir, "restore") - - // Extract into a staging directory and only move it to the restore - // directory once every entry has been extracted, so that a failed - // extraction cannot leave a partial restore directory behind, which - // would be applied on the next startup. - stagingDir, err := os.MkdirTemp(workDir, "albyhub-restore-") - if err != nil { - return fmt.Errorf("failed to create staging directory: %w", err) - } - defer os.RemoveAll(stagingDir) - extractZipEntry := func(zipFile *zip.File) error { - // Entry names come from the archive and must not be trusted. Reject any - // name that is absolute or points outside the restore directory via - // ".." segments before joining it to a path. - entryName := filepath.FromSlash(zipFile.Name) - if !filepath.IsLocal(entryName) { - return fmt.Errorf("refusing to extract zip entry outside restore directory: %q", zipFile.Name) - } - - fsFilePath := filepath.Join(stagingDir, entryName) - - // Confirm the cleaned path is still contained within the staging - // directory. - if fsFilePath != stagingDir && !strings.HasPrefix(fsFilePath, stagingDir+string(os.PathSeparator)) { - return fmt.Errorf("refusing to extract zip entry outside restore directory: %q", zipFile.Name) - } + fsFilePath := filepath.Join(workDir, "restore", filepath.FromSlash(zipFile.Name)) if err = os.MkdirAll(filepath.Dir(fsFilePath), 0700); err != nil { return fmt.Errorf("failed to create directory for zip entry: %w", err) @@ -360,20 +239,13 @@ func (api *api) RestoreBackup(unlockPassword string, r io.Reader) error { } logger.Logger.WithField("count", len(zr.File)).Info("Extracted files") - if err = os.RemoveAll(restoreDir); err != nil { - return fmt.Errorf("failed to remove existing restore directory: %w", err) - } - if err = os.Rename(stagingDir, restoreDir); err != nil { - return fmt.Errorf("failed to move extracted files to restore directory: %w", err) - } - go func() { logger.Logger.Info("Backup restored. Shutting down Alby Hub...") api.svc.Shutdown() // ensure no -shm or -wal files exist as they will stop the restore for _, filename := range []string{"nwc.db", "nwc.db-shm", "nwc.db-wal"} { err = os.Remove(filepath.Join(workDir, filename)) - if err != nil && !errors.Is(err, os.ErrNotExist) { + if err != nil { logger.Logger.WithError(err).WithField("filename", filename).Error("failed to remove old nwc db file before restore") } } @@ -387,17 +259,12 @@ func (api *api) RestoreBackup(unlockPassword string, r io.Reader) error { } func encryptingWriter(w io.Writer, password string) (io.Writer, error) { - scheme := backupCiphers[0] - - salt := make([]byte, scheme.saltSize) + salt := make([]byte, 8) if _, err := rand.Read(salt); err != nil { return nil, fmt.Errorf("failed to generate salt: %w", err) } - encKey, err := scheme.deriveKey(password, salt) - if err != nil { - return nil, fmt.Errorf("failed to derive encryption key: %w", err) - } + encKey := pbkdf2.Key([]byte(password), salt, 4096, 32, sha256.New) block, err := aes.NewCipher(encKey) if err != nil { return nil, fmt.Errorf("failed to create AES cipher: %w", err) @@ -418,8 +285,9 @@ func encryptingWriter(w io.Writer, password string) (io.Writer, error) { return nil, fmt.Errorf("failed to write IV: %w", err) } + stream := cipher.NewOFB(block, iv) cw := &cipher.StreamWriter{ - S: scheme.newStream(block, iv), + S: stream, W: w, } @@ -427,61 +295,27 @@ func encryptingWriter(w io.Writer, password string) (io.Writer, error) { } func decryptingReader(r io.Reader, password string) (io.Reader, error) { - // Read the largest possible header (salt, IV and the first bytes of the - // archive) upfront, then trial-decrypt with each supported cipher scheme - // and pick the one that produces the ZIP signature. - maxHeaderSize := 0 - minHeaderSize := math.MaxInt - for _, scheme := range backupCiphers { - headerSize := scheme.saltSize + aes.BlockSize + len(zipMagic) - maxHeaderSize = max(maxHeaderSize, headerSize) - minHeaderSize = min(minHeaderSize, headerSize) + salt := make([]byte, 8) + if _, err := io.ReadFull(r, salt); err != nil { + return nil, fmt.Errorf("failed to read salt: %w", err) } - // Read the full header with io.ReadFull rather than io.ReadAtLeast: the - // reader may deliver short reads (e.g. a network request body), and - // stopping early could truncate the header of a scheme with a larger - // salt. A short file is only acceptable if it still covers the smallest - // scheme header. - header := make([]byte, maxHeaderSize) - n, err := io.ReadFull(r, header) - if err != nil && !(errors.Is(err, io.ErrUnexpectedEOF) && n >= minHeaderSize) { - return nil, fmt.Errorf("failed to read backup header: %w", err) - } - header = header[:n] - - for _, scheme := range backupCiphers { - if len(header) < scheme.saltSize+aes.BlockSize+len(zipMagic) { - continue - } - salt := header[:scheme.saltSize] - iv := header[scheme.saltSize : scheme.saltSize+aes.BlockSize] - encrypted := header[scheme.saltSize+aes.BlockSize:] - - encKey, err := scheme.deriveKey(password, salt) - if err != nil { - return nil, fmt.Errorf("failed to derive encryption key: %w", err) - } - - block, err := aes.NewCipher(encKey) - if err != nil { - return nil, fmt.Errorf("failed to create AES cipher: %w", err) - } - - stream := scheme.newStream(block, iv) - decrypted := make([]byte, len(encrypted)) - stream.XORKeyStream(decrypted, encrypted) - if !bytes.Equal(decrypted[:len(zipMagic)], zipMagic) { - continue - } - - cr := &cipher.StreamReader{ - S: stream, - R: r, - } - - return io.MultiReader(bytes.NewReader(decrypted), cr), nil + iv := make([]byte, aes.BlockSize) + if _, err := io.ReadFull(r, iv); err != nil { + return nil, fmt.Errorf("failed to read IV: %w", err) } - return nil, errors.New("invalid unlock password or backup file") + encKey := pbkdf2.Key([]byte(password), salt, 4096, 32, sha256.New) + block, err := aes.NewCipher(encKey) + if err != nil { + return nil, fmt.Errorf("failed to create AES cipher: %w", err) + } + + stream := cipher.NewOFB(block, iv) + cr := &cipher.StreamReader{ + S: stream, + R: r, + } + + return cr, nil } diff --git a/api/backup_test.go b/api/backup_test.go deleted file mode 100644 index 9ddfcc5a..00000000 --- a/api/backup_test.go +++ /dev/null @@ -1,261 +0,0 @@ -package api - -import ( - "archive/zip" - "bytes" - "encoding/hex" - "io" - "os" - "path/filepath" - "strconv" - "strings" - "testing" - "testing/iotest" - - "github.com/sirupsen/logrus" - "github.com/stretchr/testify/require" - "gorm.io/datatypes" - - "github.com/getAlby/hub/config" - "github.com/getAlby/hub/db" - "github.com/getAlby/hub/logger" - test_db "github.com/getAlby/hub/tests/db" - "github.com/getAlby/hub/tests/mocks" -) - -// TestCreateBackup creates a backup from the test database (sqlite by -// default, postgres when TEST_DATABASE_URI is set) and verifies that the -// archive contains a valid sqlite database with the expected data. -func TestCreateBackup(t *testing.T) { - logger.Init(strconv.Itoa(int(logrus.DebugLevel))) - - workDir := t.TempDir() - - gormDB, err := test_db.NewDB(t) - require.NoError(t, err) - defer test_db.CloseDB(gormDB) - - appConfig := &config.AppConfig{ - Workdir: workDir, - DatabaseUri: test_db.GetTestDatabaseURI(), - } - cfg, err := config.NewConfig(appConfig, gormDB) - require.NoError(t, err) - - unlockPassword := "" - - // Represent a fully set-up hub: the unlock-password canary is written during - // setup and is required for the password check to pass. - require.NoError(t, cfg.SaveUnlockPasswordCheck(unlockPassword)) - - app := &db.App{ - Name: "test", - AppPubkey: "2b7dea2866958f17c568cf024e113db7a3baa9c253a9016889196b8d0b11c7ae", - Metadata: datatypes.JSON("{}"), - } - require.NoError(t, gormDB.Create(app).Error) - - lnClient := mocks.NewMockLNClient(t) - lnClient.On("GetStorageDir").Return("", nil) - lnClient.On("ResetRouter", "ALL").Return(nil) - - svc := mocks.NewMockService(t) - svc.On("GetLNClient").Return(lnClient) - svc.On("StopApp").Return() - - albyOAuthSvc := mocks.NewMockAlbyOAuthService(t) - albyOAuthSvc.On("RemoveOAuthAccessToken").Return(nil) - - theAPI := &api{ - db: gormDB, - cfg: cfg, - svc: svc, - albyOAuthSvc: albyOAuthSvc, - } - - var buf bytes.Buffer - err = theAPI.CreateBackup(unlockPassword, &buf) - require.NoError(t, err) - - // The temporary database created when converting from postgres must - // not be left behind in the working directory. - entries, err := os.ReadDir(workDir) - require.NoError(t, err) - require.Empty(t, entries) - - cr, err := decryptingReader(&buf, unlockPassword) - require.NoError(t, err) - decrypted, err := io.ReadAll(cr) - require.NoError(t, err) - - zr, err := zip.NewReader(bytes.NewReader(decrypted), int64(len(decrypted))) - require.NoError(t, err) - - dbFile, err := zr.Open("nwc.db") - require.NoError(t, err) - dbContents, err := io.ReadAll(dbFile) - require.NoError(t, err) - require.NoError(t, dbFile.Close()) - - restoredPath := filepath.Join(workDir, "restored.db") - require.NoError(t, os.WriteFile(restoredPath, dbContents, 0600)) - - restoredDB, err := db.NewDB(restoredPath, false) - require.NoError(t, err) - defer func() { - require.NoError(t, db.Stop(restoredDB)) - }() - - var restoredApp db.App - require.NoError(t, restoredDB.First(&restoredApp).Error) - require.Equal(t, app.Name, restoredApp.Name) - require.Equal(t, app.AppPubkey, restoredApp.AppPubkey) -} - -// TestRestoreBackupRejectsPathTraversal verifies that a backup archive -// containing an entry whose name points outside the restore directory is -// rejected and that no file is written outside it. -func TestRestoreBackupRejectsPathTraversal(t *testing.T) { - logger.Init(strconv.Itoa(int(logrus.DebugLevel))) - - gormDB, err := test_db.NewDB(t) - require.NoError(t, err) - defer test_db.CloseDB(gormDB) - - if gormDB.Dialector.Name() != "sqlite" { - t.Skip("restore is only supported on sqlite") - } - - workDir := t.TempDir() - - appConfig := &config.AppConfig{ - Workdir: workDir, - DatabaseUri: test_db.GetTestDatabaseURI(), - } - cfg, err := config.NewConfig(appConfig, gormDB) - require.NoError(t, err) - - theAPI := &api{ - db: gormDB, - cfg: cfg, - } - - unlockPassword := "" - - // The restore directory is /restore, so a "../" entry targets a - // file directly in the working directory, one level above it. - const escapeEntryName = "../pwned.txt" - escapeTarget := filepath.Join(workDir, "pwned.txt") - - var buf bytes.Buffer - cw, err := encryptingWriter(&buf, unlockPassword) - require.NoError(t, err) - zw := zip.NewWriter(cw) - // A valid entry before the malicious one, to verify that a partially - // extracted archive is not left behind when a later entry fails. - entryWriter, err := zw.Create("nwc.db") - require.NoError(t, err) - _, err = entryWriter.Write([]byte("backup contents")) - require.NoError(t, err) - entryWriter, err = zw.Create(escapeEntryName) - require.NoError(t, err) - _, err = entryWriter.Write([]byte("pwned")) - require.NoError(t, err) - require.NoError(t, zw.Close()) - - err = theAPI.RestoreBackup(unlockPassword, &buf) - require.ErrorContains(t, err, "refusing to extract zip entry outside restore directory") - - _, statErr := os.Stat(escapeTarget) - require.True(t, os.IsNotExist(statErr), "traversal entry must not be written outside the restore directory") - - // The failed restore must not leave a restore directory (which would be - // applied on the next startup) or any staging leftovers. - _, statErr = os.Stat(filepath.Join(workDir, "restore")) - require.True(t, os.IsNotExist(statErr), "failed restore must not leave a restore directory") - - entries, err := os.ReadDir(workDir) - require.NoError(t, err) - for _, entry := range entries { - require.False(t, strings.HasPrefix(entry.Name(), "albyhub-restore-"), "failed restore must not leave a staging directory") - } -} - -// legacyBackupFixture is a backup file created with the encryption scheme -// used by older versions (PBKDF2 key derivation), encrypted with the -// password "test-unlock-password". Its archive contains a single "nwc.db" -// entry with the contents "legacy backup contents". -const legacyBackupFixture = "0102030405060708101112131415161718191a1b1c1d1e1f8eca79631915f679a00cdd95d3f20d8d169eb9aa5d52642ca13b93886c3c7d7ba4b759462bc9dd8deccf638edcc9b5b9fda3d23dcd904cf6e99bc57ac59c4df6be5aa676542b7cbc9998029420c0ae5a6986c735150ababde5b382560acaebd5894aa4420924f1ced63fde570adc60c43b32e9e14a0ef60c379da5cac1be0000845992ea072ead036e336c7b859e8d018c4ef61667e3f520fe01" - -// TestDecryptingReaderLegacyBackup verifies that backup files created by -// older versions can still be decrypted. -func TestDecryptingReaderLegacyBackup(t *testing.T) { - encrypted, err := hex.DecodeString(legacyBackupFixture) - require.NoError(t, err) - - cr, err := decryptingReader(bytes.NewReader(encrypted), "test-unlock-password") - require.NoError(t, err) - - decrypted, err := io.ReadAll(cr) - require.NoError(t, err) - - zr, err := zip.NewReader(bytes.NewReader(decrypted), int64(len(decrypted))) - require.NoError(t, err) - - dbFile, err := zr.Open("nwc.db") - require.NoError(t, err) - dbContents, err := io.ReadAll(dbFile) - require.NoError(t, err) - require.NoError(t, dbFile.Close()) - require.Equal(t, "legacy backup contents", string(dbContents)) -} - -// TestDecryptingReaderFragmentedReader verifies that a backup file is -// decrypted correctly even when the reader delivers one byte at a time, -// which would truncate the header if it were not read in full. -func TestDecryptingReaderFragmentedReader(t *testing.T) { - var buf bytes.Buffer - cw, err := encryptingWriter(&buf, "test-unlock-password") - require.NoError(t, err) - - zw := zip.NewWriter(cw) - entryWriter, err := zw.Create("nwc.db") - require.NoError(t, err) - _, err = entryWriter.Write([]byte("backup contents")) - require.NoError(t, err) - require.NoError(t, zw.Close()) - - cr, err := decryptingReader(iotest.OneByteReader(bytes.NewReader(buf.Bytes())), "test-unlock-password") - require.NoError(t, err) - - decrypted, err := io.ReadAll(cr) - require.NoError(t, err) - - zr, err := zip.NewReader(bytes.NewReader(decrypted), int64(len(decrypted))) - require.NoError(t, err) - - dbFile, err := zr.Open("nwc.db") - require.NoError(t, err) - dbContents, err := io.ReadAll(dbFile) - require.NoError(t, err) - require.NoError(t, dbFile.Close()) - require.Equal(t, "backup contents", string(dbContents)) -} - -// TestDecryptingReaderWrongPassword verifies that decryption fails upfront -// when the password does not match the backup file. -func TestDecryptingReaderWrongPassword(t *testing.T) { - var buf bytes.Buffer - cw, err := encryptingWriter(&buf, "test-unlock-password") - require.NoError(t, err) - - zw := zip.NewWriter(cw) - entryWriter, err := zw.Create("nwc.db") - require.NoError(t, err) - _, err = entryWriter.Write([]byte("backup contents")) - require.NoError(t, err) - require.NoError(t, zw.Close()) - - _, err = decryptingReader(bytes.NewReader(buf.Bytes()), "wrong-password") - require.Error(t, err) -} diff --git a/api/models.go b/api/models.go index 355e6602..375a9556 100644 --- a/api/models.go +++ b/api/models.go @@ -8,6 +8,7 @@ import ( "github.com/getAlby/hub/alby" "github.com/getAlby/hub/db" + "github.com/getAlby/hub/lnclient" "github.com/getAlby/hub/swaps" ) @@ -22,15 +23,14 @@ type API interface { DeleteLightningAddress(ctx context.Context, appId uint) error ListChannels(ctx context.Context) ([]Channel, error) GetChannelPeerSuggestions(ctx context.Context) ([]alby.ChannelPeerSuggestion, error) - GetStories(ctx context.Context) ([]alby.Story, error) GetLSPChannelOffer(ctx context.Context) (*alby.LSPChannelOffer, error) ResetRouter(key string) error ChangeUnlockPassword(changeUnlockPasswordRequest *ChangeUnlockPasswordRequest) error SetAutoUnlockPassword(unlockPassword string) error Stop() error - GetNodeConnectionInfo(ctx context.Context) (*NodeConnectionInfo, error) - GetNodeStatus(ctx context.Context) (*NodeStatus, error) - ListPeers(ctx context.Context) ([]PeerDetails, error) + GetNodeConnectionInfo(ctx context.Context) (*lnclient.NodeConnectionInfo, error) + GetNodeStatus(ctx context.Context) (*lnclient.NodeStatus, error) + ListPeers(ctx context.Context) ([]lnclient.PeerDetails, error) ConnectPeer(ctx context.Context, connectPeerRequest *ConnectPeerRequest) error DisconnectPeer(ctx context.Context, peerId string) error OpenChannel(ctx context.Context, openChannelRequest *OpenChannelRequest) (*OpenChannelResponse, error) @@ -43,10 +43,10 @@ type API interface { SignMessage(ctx context.Context, message string) (*SignMessageResponse, error) RedeemOnchainFunds(ctx context.Context, toAddress string, amountSat uint64, feeRate *uint64, sendAll bool) (*RedeemOnchainFundsResponse, error) GetBalances(ctx context.Context) (*BalancesResponse, error) - ListTransactions(ctx context.Context, appId *uint, limit uint64, offset uint64, filters ListTransactionsFilters) (*ListTransactionsResponse, error) - ListOnchainTransactions(ctx context.Context) ([]OnchainTransaction, error) + ListTransactions(ctx context.Context, appId *uint, limit uint64, offset uint64) (*ListTransactionsResponse, error) + ListOnchainTransactions(ctx context.Context) ([]lnclient.OnchainTransaction, error) SendPayment(ctx context.Context, invoice string, amountMsat *uint64, metadata map[string]interface{}, fromAppId *uint) (*SendPaymentResponse, error) - CreateInvoice(ctx context.Context, amountMsat uint64, description string, toAppId *uint) (*MakeInvoiceResponse, error) + CreateInvoice(ctx context.Context, amountMsat uint64, description string) (*MakeInvoiceResponse, error) LookupInvoice(ctx context.Context, paymentHash string) (*LookupInvoiceResponse, error) SetTransactionUserLabels(ctx context.Context, id uint, labels map[string]string) error RequestMempoolApi(ctx context.Context, endpoint string) (interface{}, error) @@ -64,6 +64,8 @@ type API interface { MigrateNodeStorage(ctx context.Context, to string) error GetWalletCapabilities(ctx context.Context) (*WalletCapabilitiesResponse, error) Health(ctx context.Context) (*HealthResponse, error) + SetCurrency(currency string) error + SetBitcoinDisplayFormat(format string) error UpdateSettings(updateSettingsRequest *UpdateSettingsRequest) error LookupSwap(swapId string) (*LookupSwapResponse, error) ListSwaps() (*ListSwapsResponse, error) @@ -304,47 +306,38 @@ type InfoResponseRelay struct { } type InfoResponse struct { - BackendType string `json:"backendType"` - SetupCompleted bool `json:"setupCompleted"` - OAuthRedirect bool `json:"oauthRedirect"` - Running bool `json:"running"` - Unlocked bool `json:"unlocked"` - AlbyAuthUrl string `json:"albyAuthUrl"` - NextBackupReminder string `json:"nextBackupReminder"` - AlbyUserIdentifier string `json:"albyUserIdentifier"` - AlbyAccountConnected bool `json:"albyAccountConnected"` - Version string `json:"version"` - Network string `json:"network"` - EnableAdvancedSetup bool `json:"enableAdvancedSetup"` - LdkVssEnabled bool `json:"ldkVssEnabled"` - LdkVssUrl string `json:"ldkVssUrl"` - VssSupported bool `json:"vssSupported"` - DatabaseType string `json:"databaseType"` - StartupState string `json:"startupState"` - StartupError string `json:"startupError"` - StartupErrorTime time.Time `json:"startupErrorTime"` - AutoUnlockPasswordSupported bool `json:"autoUnlockPasswordSupported"` - AutoUnlockPasswordEnabled bool `json:"autoUnlockPasswordEnabled"` - Currency string `json:"currency"` - BitcoinDisplayFormat string `json:"bitcoinDisplayFormat"` - Relays []InfoResponseRelay `json:"relays"` - NodeAlias string `json:"nodeAlias"` - MempoolUrl string `json:"mempoolUrl"` - ChainDataSourceType string `json:"chainDataSourceType,omitempty"` - ChainDataSourceAddress string `json:"chainDataSourceAddress,omitempty"` - JitChannelsLiquiditySource string `json:"jitChannelsLiquiditySource,omitempty"` - JitChannelsMinPaymentSizeMsat *uint64 `json:"jitChannelsMinPaymentSizeMsat,omitempty"` - JitChannelsMaxPaymentSizeMsat *uint64 `json:"jitChannelsMaxPaymentSizeMsat,omitempty"` - JitChannelsEnabled bool `json:"jitChannelsEnabled"` - HideUpdateBanner bool `json:"hideUpdateBanner"` - SupportsBolt12 bool `json:"supportsBolt12"` - NodeMigrationFileCreated bool `json:"nodeMigrationFileCreated"` + BackendType string `json:"backendType"` + SetupCompleted bool `json:"setupCompleted"` + OAuthRedirect bool `json:"oauthRedirect"` + Running bool `json:"running"` + Unlocked bool `json:"unlocked"` + AlbyAuthUrl string `json:"albyAuthUrl"` + NextBackupReminder string `json:"nextBackupReminder"` + AlbyUserIdentifier string `json:"albyUserIdentifier"` + AlbyAccountConnected bool `json:"albyAccountConnected"` + Version string `json:"version"` + Network string `json:"network"` + EnableAdvancedSetup bool `json:"enableAdvancedSetup"` + LdkVssEnabled bool `json:"ldkVssEnabled"` + VssSupported bool `json:"vssSupported"` + StartupState string `json:"startupState"` + StartupError string `json:"startupError"` + StartupErrorTime time.Time `json:"startupErrorTime"` + AutoUnlockPasswordSupported bool `json:"autoUnlockPasswordSupported"` + AutoUnlockPasswordEnabled bool `json:"autoUnlockPasswordEnabled"` + Currency string `json:"currency"` + BitcoinDisplayFormat string `json:"bitcoinDisplayFormat"` + Relays []InfoResponseRelay `json:"relays"` + NodeAlias string `json:"nodeAlias"` + MempoolUrl string `json:"mempoolUrl"` + ChainDataSourceType string `json:"chainDataSourceType,omitempty"` + ChainDataSourceAddress string `json:"chainDataSourceAddress,omitempty"` + HideUpdateBanner bool `json:"hideUpdateBanner"` } type UpdateSettingsRequest struct { Currency string `json:"currency"` BitcoinDisplayFormat string `json:"bitcoinDisplayFormat"` - JitChannelsEnabled *bool `json:"jitChannelsEnabled"` } type SetNodeAliasRequest struct { @@ -367,68 +360,11 @@ type AutoUnlockRequest struct { UnlockPassword string `json:"unlockPassword"` } -type ConnectPeerRequest struct { - Pubkey string `json:"pubkey"` - Address string `json:"address"` - Port uint16 `json:"port"` -} - -type OpenChannelRequest struct { - Pubkey string `json:"pubkey"` - AmountSats int64 `json:"amountSats"` - Public bool `json:"public"` -} - -type OpenChannelResponse struct { - FundingTxId string `json:"fundingTxId"` -} - -type CloseChannelResponse struct { -} - -type UpdateChannelRequest struct { - ChannelId string `json:"channelId"` - NodeId string `json:"nodeId"` - ForwardingFeeBaseMsat uint32 `json:"forwardingFeeBaseMsat"` - ForwardingFeeProportionalMillionths uint32 `json:"forwardingFeeProportionalMillionths"` - MaxDustHtlcExposureFromFeeRateMultiplier uint64 `json:"maxDustHtlcExposureFromFeeRateMultiplier"` -} - -type NodeConnectionInfo struct { - Pubkey string `json:"pubkey"` - Address string `json:"address"` - Port int `json:"port"` -} - -type NodeStatus struct { - IsReady bool `json:"isReady"` - InternalNodeStatus interface{} `json:"internalNodeStatus"` -} - -type PeerDetails struct { - NodeId string `json:"nodeId"` - Address string `json:"address"` - IsPersisted bool `json:"isPersisted"` - IsConnected bool `json:"isConnected"` -} - -type OnchainTransaction struct { - AmountSat uint64 `json:"amountSat"` - CreatedAt uint64 `json:"createdAt"` - State string `json:"state"` - Type string `json:"type"` - NumConfirmations uint32 `json:"numConfirmations"` - TxId string `json:"txId"` -} - -type PendingBalanceDetails struct { - ChannelId string `json:"channelId"` - NodeId string `json:"nodeId"` - Amount uint64 `json:"amount"` // deprecated - AmountSat uint64 `json:"amountSat"` - FundingTxId string `json:"fundingTxId"` - FundingTxVout uint32 `json:"fundingTxVout"` -} +type ConnectPeerRequest = lnclient.ConnectPeerRequest +type OpenChannelRequest = lnclient.OpenChannelRequest +type OpenChannelResponse = lnclient.OpenChannelResponse +type CloseChannelResponse = lnclient.CloseChannelResponse +type UpdateChannelRequest = lnclient.UpdateChannelRequest type RebalanceChannelRequest struct { ReceiveThroughNodePubkey string `json:"receiveThroughNodePubkey"` @@ -452,45 +388,8 @@ type RedeemOnchainFundsResponse struct { TxId string `json:"txId"` } -type OnchainBalanceResponse struct { - Spendable int64 `json:"spendable"` // deprecated - SpendableSat int64 `json:"spendableSat"` - Total int64 `json:"total"` // deprecated - TotalSat int64 `json:"totalSat"` - Reserved int64 `json:"reserved"` // deprecated - ReservedSat int64 `json:"reservedSat"` - PendingBalancesFromChannelClosures uint64 `json:"pendingBalancesFromChannelClosures"` // deprecated - PendingBalancesFromChannelClosuresSat uint64 `json:"pendingBalancesFromChannelClosuresSat"` - PendingBalancesDetails []PendingBalanceDetails `json:"pendingBalancesDetails"` - PendingSweepBalancesDetails []PendingBalanceDetails `json:"pendingSweepBalancesDetails"` - InternalBalances interface{} `json:"internalBalances"` -} - -type LightningBalanceResponse struct { - TotalSpendable int64 `json:"totalSpendable"` // deprecated - TotalSpendableSat int64 `json:"totalSpendableSat"` - TotalSpendableMsat int64 `json:"totalSpendableMsat"` - TotalReceivable int64 `json:"totalReceivable"` // deprecated - TotalReceivableSat int64 `json:"totalReceivableSat"` - TotalReceivableMsat int64 `json:"totalReceivableMsat"` - NextMaxSpendable int64 `json:"nextMaxSpendable"` // deprecated - NextMaxSpendableSat int64 `json:"nextMaxSpendableSat"` - NextMaxSpendableMsat int64 `json:"nextMaxSpendableMsat"` - NextMaxReceivable int64 `json:"nextMaxReceivable"` // deprecated - NextMaxReceivableSat int64 `json:"nextMaxReceivableSat"` - NextMaxReceivableMsat int64 `json:"nextMaxReceivableMsat"` - NextMaxSpendableMPP int64 `json:"nextMaxSpendableMPP"` // deprecated - NextMaxSpendableMPPSat int64 `json:"nextMaxSpendableMPPSat"` - NextMaxSpendableMPPMsat int64 `json:"nextMaxSpendableMPPMsat"` - NextMaxReceivableMPP int64 `json:"nextMaxReceivableMPP"` // deprecated - NextMaxReceivableMPPSat int64 `json:"nextMaxReceivableMPPSat"` - NextMaxReceivableMPPMsat int64 `json:"nextMaxReceivableMPPMsat"` -} - -type BalancesResponse struct { - Onchain OnchainBalanceResponse `json:"onchain"` - Lightning LightningBalanceResponse `json:"lightning"` -} +type OnchainBalanceResponse = lnclient.OnchainBalanceResponse +type BalancesResponse = lnclient.BalancesResponse type SendPaymentResponse = Transaction type MakeInvoiceResponse = Transaction @@ -500,13 +399,6 @@ type SetTransactionUserLabelsRequest struct { Labels map[string]string `json:"labels"` } -type ListTransactionsFilters struct { - Type *string - MinAmountMsat *uint64 - HideFailed bool - SearchTerm string -} - type ListTransactionsResponse struct { TotalCount uint64 `json:"totalCount"` Transactions []Transaction `json:"transactions"` @@ -596,7 +488,6 @@ type MakeInvoiceRequest struct { AmountSat *uint64 `json:"amountSat"` AmountMsat *uint64 `json:"amountMsat"` Description string `json:"description"` - ToAppID *uint `json:"toAppId"` } type ResetRouterRequest struct { @@ -611,7 +502,7 @@ type BasicRestoreWailsRequest struct { UnlockPassword string `json:"unlockPassword"` } -type NetworkGraphResponse = interface{} +type NetworkGraphResponse = lnclient.NetworkGraphResponse type LSPOrderRequest struct { Amount *uint64 `json:"amount"` // deprecated diff --git a/api/rebalance.go b/api/rebalance.go index 2854d5b3..9afce454 100644 --- a/api/rebalance.go +++ b/api/rebalance.go @@ -122,7 +122,7 @@ func (api *api) RebalanceChannel(ctx context.Context, rebalanceChannelRequest *R return nil, err } - if paymentRequest.MSatoshi > int64(float64(amountSat)*float64(1000)*float64(1.005)+1 /*0.5% fees*/) { + if paymentRequest.MSatoshi > int64(float64(amountSat)*float64(1000)*float64(1.003)+1 /*0.3% fees*/) { return nil, errors.New("rebalance payment is more expensive than expected") } diff --git a/api/setup_test.go b/api/setup_test.go deleted file mode 100644 index 2ac03ed5..00000000 --- a/api/setup_test.go +++ /dev/null @@ -1,161 +0,0 @@ -package api - -import ( - "crypto/ecdsa" - "crypto/elliptic" - crand "crypto/rand" - "crypto/x509" - "crypto/x509/pkix" - "encoding/hex" - "encoding/pem" - "math/big" - "os" - "path/filepath" - "testing" - "time" - - "github.com/stretchr/testify/require" - "gopkg.in/macaroon.v2" -) - -// generateTestCert returns a self-signed certificate PEM block and its -// matching EC private key PEM block. -func generateTestCert(t *testing.T) (certPEM []byte, keyPEM []byte) { - t.Helper() - - key, err := ecdsa.GenerateKey(elliptic.P256(), crand.Reader) - require.NoError(t, err) - - template := x509.Certificate{ - SerialNumber: big.NewInt(1), - Subject: pkix.Name{CommonName: "test"}, - NotBefore: time.Unix(0, 0), - NotAfter: time.Unix(1<<31, 0), - } - der, err := x509.CreateCertificate(crand.Reader, &template, &template, &key.PublicKey, key) - require.NoError(t, err) - - certPEM = pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der}) - - keyDER, err := x509.MarshalPKCS8PrivateKey(key) - require.NoError(t, err) - keyPEM = pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: keyDER}) - - return certPEM, keyPEM -} - -func TestReadAndCanonicalizeLNDCert(t *testing.T) { - certPEM, keyPEM := generateTestCert(t) - dir := t.TempDir() - - t.Run("valid certificate", func(t *testing.T) { - path := filepath.Join(dir, "tls.cert") - require.NoError(t, os.WriteFile(path, certPEM, 0600)) - - got, err := readAndCanonicalizeLNDCert(path) - require.NoError(t, err) - - raw, err := hex.DecodeString(got) - require.NoError(t, err) - require.True(t, x509.NewCertPool().AppendCertsFromPEM(raw)) - }) - - t.Run("bundled private key is stripped", func(t *testing.T) { - path := filepath.Join(dir, "bundle.pem") - require.NoError(t, os.WriteFile(path, append(append([]byte{}, certPEM...), keyPEM...), 0600)) - - got, err := readAndCanonicalizeLNDCert(path) - require.NoError(t, err) - - raw, err := hex.DecodeString(got) - require.NoError(t, err) - // Only the CERTIFICATE block must survive - the private key must not - // be persisted. - require.NotContains(t, string(raw), "PRIVATE KEY") - require.Contains(t, string(raw), "CERTIFICATE") - }) - - t.Run("arbitrary non-cert file is rejected", func(t *testing.T) { - path := filepath.Join(dir, "secret.txt") - require.NoError(t, os.WriteFile(path, []byte("root:x:0:0:root:/root:/bin/bash\n"), 0600)) - - _, err := readAndCanonicalizeLNDCert(path) - require.Error(t, err) - }) - - t.Run("missing file is rejected", func(t *testing.T) { - _, err := readAndCanonicalizeLNDCert(filepath.Join(dir, "does-not-exist")) - require.Error(t, err) - }) -} - -func TestReadAndCanonicalizeLNDMacaroon(t *testing.T) { - dir := t.TempDir() - - t.Run("valid macaroon", func(t *testing.T) { - mac, err := macaroon.New([]byte("root-key"), []byte("id"), "location", macaroon.LatestVersion) - require.NoError(t, err) - raw, err := mac.MarshalBinary() - require.NoError(t, err) - - path := filepath.Join(dir, "admin.macaroon") - require.NoError(t, os.WriteFile(path, raw, 0600)) - - got, err := readAndCanonicalizeLNDMacaroon(path) - require.NoError(t, err) - - gotRaw, err := hex.DecodeString(got) - require.NoError(t, err) - roundTrip := &macaroon.Macaroon{} - require.NoError(t, roundTrip.UnmarshalBinary(gotRaw)) - }) - - t.Run("arbitrary non-macaroon file is rejected", func(t *testing.T) { - path := filepath.Join(dir, "id_rsa") - require.NoError(t, os.WriteFile(path, []byte("-----BEGIN OPENSSH PRIVATE KEY-----\nsecret\n"), 0600)) - - _, err := readAndCanonicalizeLNDMacaroon(path) - require.Error(t, err) - }) - - t.Run("missing file is rejected", func(t *testing.T) { - _, err := readAndCanonicalizeLNDMacaroon(filepath.Join(dir, "does-not-exist")) - require.Error(t, err) - }) -} - -func TestValidateCLNLightningDir(t *testing.T) { - certPEM, keyPEM := generateTestCert(t) - - writeCLNDir := func(t *testing.T, dir string) { - t.Helper() - require.NoError(t, os.WriteFile(filepath.Join(dir, "ca.pem"), certPEM, 0600)) - require.NoError(t, os.WriteFile(filepath.Join(dir, "client.pem"), certPEM, 0600)) - require.NoError(t, os.WriteFile(filepath.Join(dir, "client-key.pem"), keyPEM, 0600)) - } - - t.Run("valid directory", func(t *testing.T) { - dir := t.TempDir() - writeCLNDir(t, dir) - require.NoError(t, validateCLNLightningDir(dir, false)) - }) - - t.Run("valid directory with hold subdirectory", func(t *testing.T) { - dir := t.TempDir() - writeCLNDir(t, dir) - holdDir := filepath.Join(dir, "hold") - require.NoError(t, os.Mkdir(holdDir, 0700)) - writeCLNDir(t, holdDir) - require.NoError(t, validateCLNLightningDir(dir, true)) - }) - - t.Run("hold requested but subdirectory missing", func(t *testing.T) { - dir := t.TempDir() - writeCLNDir(t, dir) - require.Error(t, validateCLNLightningDir(dir, true)) - }) - - t.Run("arbitrary directory is rejected", func(t *testing.T) { - require.Error(t, validateCLNLightningDir(t.TempDir(), false)) - }) -} diff --git a/api/transactions.go b/api/transactions.go index eb5109b4..b1a530e9 100644 --- a/api/transactions.go +++ b/api/transactions.go @@ -4,29 +4,20 @@ import ( "context" "encoding/json" "errors" - "fmt" - "net/url" - "strconv" "strings" "time" - "github.com/getAlby/hub/constants" "github.com/getAlby/hub/logger" "github.com/getAlby/hub/transactions" "github.com/sirupsen/logrus" ) -func (api *api) CreateInvoice(ctx context.Context, amountMsat uint64, description string, toAppId *uint) (*MakeInvoiceResponse, error) { +func (api *api) CreateInvoice(ctx context.Context, amountMsat uint64, description string) (*MakeInvoiceResponse, error) { lnClient := api.svc.GetLNClient() if lnClient == nil { return nil, ErrLNClientNotStarted } - - if toAppId != nil && api.appsSvc.GetAppById(*toAppId) == nil { - return nil, errors.New("app does not exist") - } - - transaction, err := api.svc.GetTransactionsService().MakeInvoice(ctx, amountMsat, description, "", 0, nil, lnClient, toAppId, nil, nil) + transaction, err := api.svc.GetTransactionsService().MakeInvoice(ctx, amountMsat, description, "", 0, nil, lnClient, nil, nil, nil) if err != nil { return nil, err } @@ -49,47 +40,7 @@ func (api *api) SetTransactionUserLabels(ctx context.Context, id uint, labels ma return api.svc.GetTransactionsService().SetTransactionUserLabels(ctx, id, labels) } -// ParseListTransactionsFilters parses transaction filter query parameters -// shared by the HTTP and Wails transports. Invalid values return an error. -func ParseListTransactionsFilters(query url.Values) (ListTransactionsFilters, error) { - filters := ListTransactionsFilters{} - - if transactionType := query.Get("type"); transactionType != "" { - if transactionType != constants.TRANSACTION_TYPE_INCOMING && transactionType != constants.TRANSACTION_TYPE_OUTGOING { - return filters, fmt.Errorf("invalid type: %s", transactionType) - } - filters.Type = &transactionType - } - - if minAmountSatParam := query.Get("minAmountSat"); minAmountSatParam != "" { - minAmountSat, err := strconv.ParseUint(minAmountSatParam, 10, 64) - if err != nil || minAmountSat == 0 { - return filters, fmt.Errorf("invalid minAmountSat: %s", minAmountSatParam) - } - - const msatPerSat = uint64(1000) - if minAmountSat > ^uint64(0)/msatPerSat { - return filters, fmt.Errorf("minAmountSat is too large") - } - - minAmountMsat := minAmountSat * msatPerSat - filters.MinAmountMsat = &minAmountMsat - } - - if hideFailedParam := query.Get("hideFailed"); hideFailedParam != "" { - hideFailed, err := strconv.ParseBool(hideFailedParam) - if err != nil { - return filters, fmt.Errorf("invalid hideFailed: %s", hideFailedParam) - } - filters.HideFailed = hideFailed - } - - filters.SearchTerm = strings.TrimSpace(query.Get("search")) - - return filters, nil -} - -func (api *api) ListTransactions(ctx context.Context, appId *uint, limit uint64, offset uint64, filters ListTransactionsFilters) (*ListTransactionsResponse, error) { +func (api *api) ListTransactions(ctx context.Context, appId *uint, limit uint64, offset uint64) (*ListTransactionsResponse, error) { lnClient := api.svc.GetLNClient() if lnClient == nil { return nil, ErrLNClientNotStarted @@ -100,18 +51,13 @@ func (api *api) ListTransactions(ctx context.Context, appId *uint, limit uint64, forceFilterByAppId = true } - dbTransactions, totalCount, err := api.svc.GetTransactionsService().ListTransactions(ctx, 0, 0, limit, offset, true, false, lnClient, appId, forceFilterByAppId, &transactions.ListTransactionsFilters{ - Type: filters.Type, - MinAmountMsat: filters.MinAmountMsat, - HideFailed: filters.HideFailed, - SearchTerm: filters.SearchTerm, - }) + transactions, totalCount, err := api.svc.GetTransactionsService().ListTransactions(ctx, 0, 0, limit, offset, true, false, nil, lnClient, appId, forceFilterByAppId) if err != nil { return nil, err } apiTransactions := []Transaction{} - for _, transaction := range dbTransactions { + for _, transaction := range transactions { apiTransactions = append(apiTransactions, *toApiTransaction(&transaction)) } diff --git a/api/transactions_test.go b/api/transactions_test.go deleted file mode 100644 index 934ef46f..00000000 --- a/api/transactions_test.go +++ /dev/null @@ -1,98 +0,0 @@ -package api - -import ( - "context" - "net/url" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/getAlby/hub/tests" - "github.com/getAlby/hub/tests/mocks" - "github.com/getAlby/hub/transactions" -) - -func TestCreateInvoice_ToApp(t *testing.T) { - ctx := context.TODO() - - testSvc, err := tests.CreateTestService(t) - require.NoError(t, err) - defer testSvc.Remove() - - app, _, err := tests.CreateApp(testSvc) - require.NoError(t, err) - - svc := mocks.NewMockService(t) - svc.On("GetLNClient").Return(testSvc.LNClient) - svc.On("GetTransactionsService").Return(transactions.NewTransactionsService(testSvc.DB, testSvc.EventPublisher)) - - theAPI := &api{ - appsSvc: testSvc.AppsService, - svc: svc, - } - - transaction, err := theAPI.CreateInvoice(ctx, 1000, "Hello world", &app.ID) - - require.NoError(t, err) - require.NotNil(t, transaction.AppId) - assert.Equal(t, app.ID, *transaction.AppId) -} - -func TestCreateInvoice_ToAppNotFound(t *testing.T) { - ctx := context.TODO() - - testSvc, err := tests.CreateTestService(t) - require.NoError(t, err) - defer testSvc.Remove() - - svc := mocks.NewMockService(t) - svc.On("GetLNClient").Return(testSvc.LNClient) - - theAPI := &api{ - appsSvc: testSvc.AppsService, - svc: svc, - } - - missingAppId := uint(999) - transaction, err := theAPI.CreateInvoice(ctx, 1000, "Hello world", &missingAppId) - - assert.Nil(t, transaction) - require.Error(t, err) - assert.Equal(t, "app does not exist", err.Error()) -} - -func TestParseListTransactionsFilters(t *testing.T) { - minAmountMsat := uint64(1000_000) - outgoing := "outgoing" - - filters, err := ParseListTransactionsFilters(url.Values{ - "type": {"outgoing"}, - "minAmountSat": {"1000"}, - "hideFailed": {"true"}, - "search": {" coffee "}, - }) - require.NoError(t, err) - assert.Equal(t, ListTransactionsFilters{ - Type: &outgoing, - MinAmountMsat: &minAmountMsat, - HideFailed: true, - SearchTerm: "coffee", - }, filters) - - filters, err = ParseListTransactionsFilters(url.Values{}) - require.NoError(t, err) - assert.Equal(t, ListTransactionsFilters{}, filters) - - for _, invalidQuery := range []url.Values{ - {"type": {"sideways"}}, - {"minAmountSat": {"abc"}}, - {"minAmountSat": {"-1"}}, - {"minAmountSat": {"0"}}, - {"minAmountSat": {"18446744073709551615"}}, - {"hideFailed": {"maybe"}}, - } { - _, err = ParseListTransactionsFilters(invalidQuery) - assert.Error(t, err, "query: %v", invalidQuery) - } -} diff --git a/apps/apps_service.go b/apps/apps_service.go index 62c53b98..c0bbda7c 100644 --- a/apps/apps_service.go +++ b/apps/apps_service.go @@ -58,11 +58,9 @@ func (svc *appsService) CreateApp(name string, pubkey string, maxAmountSat uint6 backendType, _ := svc.cfg.Get("LNBackendType", "") if backendType != config.LDKBackendType && backendType != config.LNDBackendType && - backendType != config.PhoenixBackendType && - backendType != config.BarkBackendType && - backendType != config.CLNBackendType { + backendType != config.PhoenixBackendType { return nil, "", fmt.Errorf( - "sub-wallets are currently not supported on your node backend. Try LDK, LND, PHOENIX, BARK, or CLN") + "sub-wallets are currently not supported on your node backend. Try LDK or LND") } } @@ -186,17 +184,11 @@ func (svc *appsService) DeleteApp(app *db.App) error { if err != nil { return err } - walletPubkey := "" - if app.WalletPubkey != nil { - // only exists for non-legacy apps - walletPubkey = *app.WalletPubkey - } svc.eventPublisher.Publish(&events.Event{ Event: "nwc_app_deleted", Properties: map[string]interface{}{ - "name": app.Name, - "id": app.ID, - "walletPubkey": walletPubkey, + "name": app.Name, + "id": app.ID, }, }) return nil diff --git a/apps/tests/apps_service_test.go b/apps/tests/apps_service_test.go index c4690fa0..ee4a413d 100644 --- a/apps/tests/apps_service_test.go +++ b/apps/tests/apps_service_test.go @@ -54,5 +54,5 @@ func TestHandleCreateApp_IsolatedUnsupportedBackendType(t *testing.T) { assert.Nil(t, app) assert.Equal(t, "", secretKey) require.Error(t, err) - assert.Equal(t, "sub-wallets are currently not supported on your node backend. Try LDK, LND, PHOENIX, BARK, or CLN", err.Error()) + assert.Equal(t, "sub-wallets are currently not supported on your node backend. Try LDK or LND", err.Error()) } diff --git a/cmd/db_migrate/main.go b/cmd/db_migrate/main.go index 4207a4c6..c10f4a3e 100644 --- a/cmd/db_migrate/main.go +++ b/cmd/db_migrate/main.go @@ -2,7 +2,9 @@ package main import ( "flag" + "fmt" "os" + "slices" "strconv" "github.com/sirupsen/logrus" @@ -12,6 +14,18 @@ import ( "github.com/getAlby/hub/logger" ) +var expectedTables = []string{ + "apps", + "app_permissions", + "request_events", + "response_events", + "transactions", + "swaps", + "user_configs", + "migrations", + "forwards", +} + func main() { var fromDSN, toDSN string @@ -50,29 +64,54 @@ func main() { } defer stopDB(toDB) - // When migrating to Postgres (e.g. a cloud deployment) the node data must - // be stored in VSS, since only the database is migrated by this tool. - if toDB.Dialector.Name() == "postgres" { - var vssConfig db.UserConfig - result := fromDB.Where("key = ?", "LdkVssEnabled").First(&vssConfig) - if result.Error != nil { - if result.Error == gorm.ErrRecordNotFound { - logger.Logger.Error("LdkVssEnabled config not found in source DB. Migration will not proceed.") - } else { - logger.Logger.WithError(result.Error).Error("failed to query LdkVssEnabled config from source DB") - } - os.Exit(1) - } + // Migrations are applied to both the source and the target DB, so + // schemas should be equal at this point. + err = checkSchema(fromDB) + if err != nil { + logger.Logger.WithError(err).Error("database schema check failed; the migration tool may be outdated") + os.Exit(1) + } - if vssConfig.Value != "true" { - logger.Logger.Error("VSS is not enabled in the source DB (LdkVssEnabled is not 'true'). Migration will not proceed.") - os.Exit(1) + // Check if VSS is enabled in the source database + var vssConfig db.UserConfig + result := fromDB.Where("key = ?", "LdkVssEnabled").First(&vssConfig) + if result.Error != nil { + if result.Error == gorm.ErrRecordNotFound { + logger.Logger.Error("LdkVssEnabled config not found in source DB. Migration will not proceed.") + } else { + logger.Logger.WithError(result.Error).Error("failed to query LdkVssEnabled config from source DB") } - logger.Logger.Info("LdkVssEnabled check passed.") + os.Exit(1) + } + + if vssConfig.Value != "true" { + logger.Logger.Error("VSS is not enabled in the source DB (LdkVssEnabled is not 'true'). Migration will not proceed.") + os.Exit(1) + } + logger.Logger.Info("LdkVssEnabled check passed.") + + // NOTE: we assume that excess request events have already been cleaned up due to the background task + // and only a maximum of ~1000 remain. + logger.Logger.Info("Deleting orphaned request events.") + err = fromDB.Exec("DELETE FROM request_events WHERE app_id NOT IN (SELECT id FROM apps);").Error + + if err != nil { + logger.Logger.WithError(err).Error("failed to delete orphaned request events") + os.Exit(1) + } + + // NOTE: we assume that excess response events have already been cleaned up due to the background task + // and only a maximum of ~1000 remain. + logger.Logger.Info("Deleting orphaned response events.") + err = fromDB.Exec("DELETE FROM response_events WHERE request_id NOT IN (SELECT id FROM request_events);").Error + + if err != nil { + logger.Logger.WithError(err).Error("failed to delete orphaned response events") + os.Exit(1) } logger.Logger.Info("migrating...") - err = db.MigrateDB(fromDB, toDB) + err = migrateDB(fromDB, toDB) if err != nil { logger.Logger.WithError(err).Error("failed to migrate database") os.Exit(1) @@ -80,3 +119,175 @@ func main() { logger.Logger.Info("migration complete") } + +func migrateDB(from, to *gorm.DB) error { + tx := to.Begin() + defer tx.Rollback() + + if err := tx.Error; err != nil { + return fmt.Errorf("failed to start transaction: %w", err) + } + + // Table migration order matters: referenced tables must be migrated + // before referencing tables. + + logger.Logger.Info("migrating apps...") + if err := migrateTable[db.App](from, tx); err != nil { + return fmt.Errorf("failed to migrate apps: %w", err) + } + + logger.Logger.Info("migrating app_permissions...") + if err := migrateTable[db.AppPermission](from, tx); err != nil { + return fmt.Errorf("failed to migrate app_permissions: %w", err) + } + + logger.Logger.Info("migrating request_events...") + if err := migrateTable[db.RequestEvent](from, tx); err != nil { + return fmt.Errorf("failed to migrate request_events: %w", err) + } + + logger.Logger.Info("migrating response_events...") + if err := migrateTable[db.ResponseEvent](from, tx); err != nil { + return fmt.Errorf("failed to migrate response_events: %w", err) + } + + logger.Logger.Info("migrating transactions...") + if err := migrateTable[db.Transaction](from, tx); err != nil { + return fmt.Errorf("failed to migrate transactions: %w", err) + } + + logger.Logger.Info("migrating user_configs...") + if err := migrateTable[db.UserConfig](from, tx); err != nil { + return fmt.Errorf("failed to migrate user_configs: %w", err) + } + + if to.Dialector.Name() == "postgres" { + logger.Logger.Info("resetting sequences...") + if err := resetSequences(tx); err != nil { + return fmt.Errorf("failed to reset sequences: %w", err) + } + } + + tx.Commit() + if err := tx.Error; err != nil { + return fmt.Errorf("failed to commit transaction: %w", err) + } + + return nil +} + +func migrateTable[T any](from, to *gorm.DB) error { + var data []T + if err := from.Find(&data).Error; err != nil { + return fmt.Errorf("failed to fetch data: %w", err) + } + + if len(data) == 0 { + return nil + } + + // to avoid "failed to migrate transactions: failed to insert data: extended protocol limited to 65535 parameters" + // see https://stackoverflow.com/questions/77372430/extended-protocol-limited-to-65535-parameters-golang-gorm + // max statements is 65535 + // but it's the number of records * columns + // to be safe, using a lower value of 1000. + // this will fail if any table has more than 65 columns, which I doubt we will have + max := 1000 + for i := 0; i < len(data); i += max { + j := min(i+max, len(data)) + + if err := to.Create(data[i:j]).Error; err != nil { + return fmt.Errorf("failed to insert data: %w", err) + } + } + + return nil +} + +func checkSchema(db *gorm.DB) error { + tables, err := listTables(db) + if err != nil { + return fmt.Errorf("failed to list database tables: %w", err) + } + + for _, table := range expectedTables { + if !slices.Contains(tables, table) { + return fmt.Errorf("table missing from the database: %q", table) + } + } + + for _, table := range tables { + if !slices.Contains(expectedTables, table) { + return fmt.Errorf("unexpected table found in the database: %q", table) + } + } + + return nil +} + +func listTables(db *gorm.DB) ([]string, error) { + var query string + + switch db.Dialector.Name() { + case "sqlite": + query = "SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%';" + case "postgres": + query = "SELECT tablename FROM pg_tables WHERE schemaname = 'public';" + default: + return nil, fmt.Errorf("unsupported database: %q", db.Dialector.Name()) + } + + rows, err := db.Raw(query).Rows() + if err != nil { + return nil, fmt.Errorf("failed to query table names: %w", err) + } + defer func() { + if err := rows.Close(); err != nil { + logger.Logger.WithError(err).Error("failed to close rows") + } + }() + + var tables []string + for rows.Next() { + var table string + if err := rows.Scan(&table); err != nil { + return nil, fmt.Errorf("failed to scan table name: %w", err) + } + tables = append(tables, table) + } + + return tables, nil +} + +func resetSequences(db *gorm.DB) error { + type resetReq struct { + table string + seq string + } + + resetReqs := []resetReq{ + {"apps", "apps_2_id_seq"}, + {"app_permissions", "app_permissions_2_id_seq"}, + {"request_events", "request_events_id_seq"}, + {"response_events", "response_events_id_seq"}, + {"transactions", "transactions_id_seq"}, + {"user_configs", "user_configs_id_seq"}, + } + + for _, req := range resetReqs { + if err := resetPostgresSequence(db, req.table, req.seq); err != nil { + return fmt.Errorf("failed to reset sequence %q for %q: %w", req.seq, req.table, err) + } + } + + return nil +} + +func resetPostgresSequence(db *gorm.DB, table string, seq string) error { + query := fmt.Sprintf("SELECT setval('%s', (SELECT MAX(id) FROM %s));", seq, table) + if err := db.Exec(query).Error; err != nil { + return fmt.Errorf("failed to execute setval(): %w", err) + } + + return nil +} diff --git a/cmd/db_migrate/migrate_test.go b/cmd/db_migrate/migrate_test.go index 12ae54e1..ecc6a78d 100644 --- a/cmd/db_migrate/migrate_test.go +++ b/cmd/db_migrate/migrate_test.go @@ -30,6 +30,40 @@ func (e *testEnvironment) cleanup(t *testing.T) { require.NoError(t, err) } +func TestSchemaCheck(t *testing.T) { + type testCase struct { + name string + uri string + } + + tc := []testCase{ + { + name: "schema check sqlite", + uri: getTestSqliteURI(0), + }, + } + + if pgUri := getTestPostgresURI(); pgUri != "" { + tc = append(tc, testCase{ + name: "schema check postgres", + uri: pgUri, + }) + } + + logger.Init(strconv.Itoa(int(logrus.DebugLevel))) + + for _, tt := range tc { + t.Run(tt.name, func(t *testing.T) { + dbConn, err := test_db.NewDBWithURI(t, tt.uri) + require.NoError(t, err) + defer db.Stop(dbConn) + + err = checkSchema(dbConn) + require.NoError(t, err) + }) + } +} + func TestMigrate(t *testing.T) { type testCase struct { name string @@ -70,17 +104,8 @@ func TestMigrate(t *testing.T) { require.NoError(t, err) defer env.cleanup(t) - err = db.MigrateDB(env.source, env.dest) + err = migrateDB(env.source, env.dest) require.NoError(t, err) - - requireCount[db.App](t, env.dest, 2) - requireCount[db.AppPermission](t, env.dest, 2) - requireCount[db.RequestEvent](t, env.dest, 1) - requireCount[db.ResponseEvent](t, env.dest, 1) - requireCount[db.Transaction](t, env.dest, 1) - requireCount[db.Swap](t, env.dest, 1) - requireCount[db.Forward](t, env.dest, 1) - requireCount[db.UserConfig](t, env.dest, 1) }) } } @@ -175,83 +200,6 @@ func insertMockData(t *testing.T, tx *gorm.DB) { UpdatedAt: baseTime, } create(t, tx, app2Perm) - - requestEvent1 := &db.RequestEvent{ - AppId: &app1.ID, - NostrId: "a35a1ca6d1a06e08a509f2c8fe3edb2ba10811d030e2f6f3239e9f21203ac954", - ContentData: "{}", - Method: "pay_invoice", - State: "executed", - CreatedAt: baseTime, - UpdatedAt: baseTime, - } - create(t, tx, requestEvent1) - - responseEvent1 := &db.ResponseEvent{ - NostrId: "e30d55d0e4f0d5391a1a1379f1d8b7d38ad02b3554b06ca993aa8790a3153f61", - RequestId: requestEvent1.ID, - State: "confirmed", - RepliedAt: baseTime, - CreatedAt: baseTime, - UpdatedAt: baseTime, - } - create(t, tx, responseEvent1) - - transaction1 := &db.Transaction{ - AppId: &app1.ID, - RequestEventId: &requestEvent1.ID, - Type: "outgoing", - State: "settled", - AmountMsat: 21000, - FeeMsat: 1000, - PaymentRequest: "lnbc210n1invoice", - PaymentHash: "13d9764a54269fa4d5f4e7c410f4ffdbc839bbeaa2fcbb96343ca502f0c86e34", - Description: "test transaction", - Preimage: ptr("2c1ee1b464b1a1a147debe0ac0c8ce4b615f9bfa64d12a25c1c4d10ea45a5b02"), - CreatedAt: baseTime, - UpdatedAt: baseTime, - SettledAt: &baseTime, - Metadata: datatypes.JSON("{}"), - Boostagram: datatypes.JSON("{}"), - } - create(t, tx, transaction1) - - swap1 := &db.Swap{ - SwapId: "swap1", - Type: "out", - State: "success", - Invoice: "lnbc210n1swapinvoice", - SendAmountSat: 21000, - ReceiveAmountSat: 20000, - Preimage: "35a3f1a7a06a41b9ba3a1b1a8ff852e5085b3b593f8ba4677a35a1ca6d1a06e0", - PaymentHash: "e6b1a1379f1d8b7d38ad02b3554b06ca993aa8790a3153f61e30d55d0e4f0d53", - DestinationAddress: "bc1qtest", - LockupAddress: "bc1qlockup", - LockupTxId: "lockuptx", - ClaimTxId: "claimtx", - AutoSwap: false, - TimeoutBlockHeight: 900000, - BoltzPubkey: "02d1a06e08a509f2c8fe3edb2ba10811d030e2f6f3239e9f21203ac954a35a1c", - SwapTree: datatypes.JSON("{}"), - CreatedAt: baseTime, - UpdatedAt: baseTime, - } - create(t, tx, swap1) - - forward1 := &db.Forward{ - OutboundAmountForwardedMsat: 1000000, - TotalFeeEarnedMsat: 1000, - CreatedAt: baseTime, - UpdatedAt: baseTime, - } - create(t, tx, forward1) -} - -func requireCount[T any](t *testing.T, tx *gorm.DB, expected int64) { - var count int64 - var model T - require.NoError(t, tx.Model(&model).Count(&count).Error) - require.Equal(t, expected, count) } func create[T any](t *testing.T, tx *gorm.DB, v T) *gorm.DB { diff --git a/config/config.go b/config/config.go index c0434665..57a17685 100644 --- a/config/config.go +++ b/config/config.go @@ -408,18 +408,7 @@ func (cfg *config) SetAutoUnlockPassword(unlockPassword string) error { func (cfg *config) CheckUnlockPassword(encryptionKey string) bool { decryptedValue, err := cfg.Get("UnlockPasswordCheck", encryptionKey) - // require a non-empty match so an absent or empty canary always fails - return err == nil && decryptedValue != "" && decryptedValue == unlockPasswordCheck -} - -func (cfg *config) IsUnlockPasswordCheckSet() (bool, error) { - // Read the raw value with an empty encryption key so we can detect the - // presence of the canary row without needing the (possibly wrong) password. - value, err := cfg.Get("UnlockPasswordCheck", "") - if err != nil { - return false, fmt.Errorf("read unlock password check: %w", err) - } - return value != "", nil + return err == nil && (decryptedValue == "" || decryptedValue == unlockPasswordCheck) } func (cfg *config) SaveUnlockPasswordCheck(encryptionKey string) error { diff --git a/config/config_test.go b/config/config_test.go index 1849e45a..302c120c 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -56,6 +56,8 @@ func TestCheckUnlockPasswordCache(t *testing.T) { Workdir: ".test", }, db) require.NoError(t, err) + err = cfg.ChangeUnlockPassword("", unlockPassword) + require.NoError(t, err) err = cfg.SaveUnlockPasswordCheck(unlockPassword) require.NoError(t, err) diff --git a/config/models.go b/config/models.go index 48860104..84256b3d 100644 --- a/config/models.go +++ b/config/models.go @@ -6,7 +6,6 @@ const ( PhoenixBackendType = "PHOENIX" CashuBackendType = "CASHU" CLNBackendType = "CLN" - BarkBackendType = "BARK" ) const ( @@ -38,7 +37,6 @@ type AppConfig struct { LDKMaxPathCount uint8 `envconfig:"LDK_MAX_PATH_COUNT" default:"5"` LDKChannelMonitorWarningSizeBytes uint64 `envconfig:"LDK_CHANNEL_MONITOR_WARNING_SIZE_BYTES" default:"5000000"` LDKVssUrl string `envconfig:"LDK_VSS_URL" default:"https://vss.getalbypro.com/vss"` - LDKLiquiditySourceLsps2 string `envconfig:"LDK_LSPS2_ADDRESSES"` LDKListeningAddresses string `envconfig:"LDK_LISTENING_ADDRESSES" default:"[::]:9735"` LDKAnnouncementAddresses string `envconfig:"LDK_ANNOUNCEMENT_ADDRESSES"` LDKTransientNetworkGraph bool `envconfig:"LDK_TRANSIENT_NETWORK_GRAPH" default:"false"` @@ -65,10 +63,6 @@ type AppConfig struct { CLNAddress string `envconfig:"CLN_ADDRESS"` CLNLightningDir string `envconfig:"CLN_LIGHTNING_DIR"` CLNAddressHold string `envconfig:"CLN_ADDRESS_HOLD"` - BarkServer string `envconfig:"BARK_SERVER" default:"https://ark.second.tech"` - BarkEsploraServer string `envconfig:"BARK_ESPLORA_SERVER" default:"https://mempool.second.tech/api"` - BarkServerAccessToken string `envconfig:"BARK_SERVER_ACCESS_TOKEN"` - BarkLogLevel string `envconfig:"BARK_LOG_LEVEL" default:"3"` } func (c *AppConfig) IsDefaultClientId() bool { @@ -94,7 +88,6 @@ type Config interface { GetMempoolUrl() string GetEnv() *AppConfig CheckUnlockPassword(password string) bool - IsUnlockPasswordCheckSet() (bool, error) ChangeUnlockPassword(currentUnlockPassword string, newUnlockPassword string) error SetAutoUnlockPassword(unlockPassword string) error SaveUnlockPasswordCheck(encryptionKey string) error diff --git a/config/tests/config_test.go b/config/tests/config_test.go index c256f542..ae1bfd0d 100644 --- a/config/tests/config_test.go +++ b/config/tests/config_test.go @@ -16,6 +16,8 @@ func TestCheckUnlockPasswordCache_InvalidSecond(t *testing.T) { require.NoError(t, err) defer svc.Remove() + err = svc.Cfg.ChangeUnlockPassword("", unlockPassword) + require.NoError(t, err) err = svc.Cfg.SaveUnlockPasswordCheck(unlockPassword) require.NoError(t, err) @@ -33,6 +35,8 @@ func TestCheckUnlockPasswordCache_InvalidFirst(t *testing.T) { require.NoError(t, err) defer svc.Remove() + err = svc.Cfg.ChangeUnlockPassword("", unlockPassword) + require.NoError(t, err) err = svc.Cfg.SaveUnlockPasswordCheck(unlockPassword) require.NoError(t, err) @@ -54,6 +58,8 @@ func TestCheckUnlockPassword_ChangePassword(t *testing.T) { require.NoError(t, err) defer svc.Remove() + err = svc.Cfg.ChangeUnlockPassword("", unlockPassword) + require.NoError(t, err) err = svc.Cfg.SaveUnlockPasswordCheck(unlockPassword) require.NoError(t, err) @@ -75,50 +81,6 @@ func TestCheckUnlockPassword_ChangePassword(t *testing.T) { assert.True(t, svc.Cfg.CheckUnlockPassword(newUnlockPassword)) } -func TestCheckUnlockPassword_MissingCanaryFailsClosed(t *testing.T) { - svc, err := tests.CreateTestService(t) - require.NoError(t, err) - defer svc.Remove() - - // A fresh hub has not saved the unlock-password canary yet. - set, err := svc.Cfg.IsUnlockPasswordCheckSet() - require.NoError(t, err) - assert.False(t, set) - - // Without the canary, no password may validate - including an empty one. - assert.False(t, svc.Cfg.CheckUnlockPassword("")) - assert.False(t, svc.Cfg.CheckUnlockPassword("any-password")) - - // After the canary is saved, only the correct password validates. - err = svc.Cfg.SaveUnlockPasswordCheck("correct") - require.NoError(t, err) - - set, err = svc.Cfg.IsUnlockPasswordCheckSet() - require.NoError(t, err) - assert.True(t, set) - - assert.True(t, svc.Cfg.CheckUnlockPassword("correct")) - assert.False(t, svc.Cfg.CheckUnlockPassword("wrong")) - assert.False(t, svc.Cfg.CheckUnlockPassword("")) -} - -func TestCheckUnlockPassword_NoPasswordHub(t *testing.T) { - svc, err := tests.CreateTestService(t) - require.NoError(t, err) - defer svc.Remove() - - // A hub configured without an unlock password stores the canary unencrypted; - // the empty password must still validate after the fail-closed change. - err = svc.Cfg.SaveUnlockPasswordCheck("") - require.NoError(t, err) - - set, err := svc.Cfg.IsUnlockPasswordCheckSet() - require.NoError(t, err) - assert.True(t, set) - - assert.True(t, svc.Cfg.CheckUnlockPassword("")) -} - func TestSetIgnore_NoEncryptionKey(t *testing.T) { svc, err := tests.CreateTestService(t) require.NoError(t, err) @@ -257,7 +219,7 @@ func TestJWTSecret_GeneratedOnLoad(t *testing.T) { cfg, err := config.NewConfig(&config.AppConfig{}, svc.DB) require.NoError(t, err) - err = cfg.SaveUnlockPasswordCheck("123") + err = cfg.ChangeUnlockPassword("", "123") require.NoError(t, err) err = cfg.LoadJWTSecret("123") @@ -289,6 +251,9 @@ func TestJWTSecret_WrongPassword(t *testing.T) { cfg, err := config.NewConfig(&config.AppConfig{}, svc.DB) require.NoError(t, err) + err = cfg.ChangeUnlockPassword("", "123") + require.NoError(t, err) + err = cfg.SaveUnlockPasswordCheck("123") require.NoError(t, err) @@ -307,7 +272,7 @@ func TestJWTSecret_ChangePassword(t *testing.T) { cfg, err := config.NewConfig(&config.AppConfig{}, svc.DB) require.NoError(t, err) - err = cfg.SaveUnlockPasswordCheck("123") + err = cfg.ChangeUnlockPassword("", "123") require.NoError(t, err) err = cfg.LoadJWTSecret("123") @@ -317,7 +282,7 @@ func TestJWTSecret_ChangePassword(t *testing.T) { require.NoError(t, err) assert.NotEmpty(t, jwtSecret) - err = cfg.ChangeUnlockPassword("123", "1234") + err = cfg.ChangeUnlockPassword("", "1234") require.NoError(t, err) newJwtSecret, err := cfg.GetJWTSecret() @@ -341,7 +306,7 @@ func TestJWTSecret_ReplaceUnencryptedSecretOnLoad(t *testing.T) { cfg, err := config.NewConfig(&config.AppConfig{}, svc.DB) require.NoError(t, err) - err = cfg.SaveUnlockPasswordCheck("123") + err = cfg.ChangeUnlockPassword("", "123") require.NoError(t, err) // simulate a hub that had an unencrypted JWT secret diff --git a/db/db_migrate.go b/db/db_migrate.go deleted file mode 100644 index 666e77c3..00000000 --- a/db/db_migrate.go +++ /dev/null @@ -1,235 +0,0 @@ -package db - -import ( - "fmt" - "slices" - - "gorm.io/gorm" - - "github.com/getAlby/hub/logger" -) - -var expectedTables = []string{ - "apps", - "app_permissions", - "request_events", - "response_events", - "transactions", - "swaps", - "user_configs", - "migrations", - "forwards", -} - -// MigrateDB copies all rows from one database to another. Both databases -// must have an up-to-date schema (they are checked against expectedTables). -// Orphaned request and response events are deleted from the source database -// before copying, as they would violate foreign key constraints in the -// destination database. -func MigrateDB(from, to *gorm.DB) error { - if err := checkSchema(from); err != nil { - return fmt.Errorf("source database schema check failed: %w", err) - } - - if err := checkSchema(to); err != nil { - return fmt.Errorf("destination database schema check failed: %w", err) - } - - // NOTE: we assume that excess request events have already been cleaned up due to the background task - // and only a maximum of ~1000 remain. - logger.Logger.Info("Deleting orphaned request events.") - err := from.Exec("DELETE FROM request_events WHERE app_id NOT IN (SELECT id FROM apps);").Error - if err != nil { - return fmt.Errorf("failed to delete orphaned request events: %w", err) - } - - // NOTE: we assume that excess response events have already been cleaned up due to the background task - // and only a maximum of ~1000 remain. - logger.Logger.Info("Deleting orphaned response events.") - err = from.Exec("DELETE FROM response_events WHERE request_id NOT IN (SELECT id FROM request_events);").Error - if err != nil { - return fmt.Errorf("failed to delete orphaned response events: %w", err) - } - - tx := to.Begin() - defer tx.Rollback() - - if err := tx.Error; err != nil { - return fmt.Errorf("failed to start transaction: %w", err) - } - - // Table migration order matters: referenced tables must be migrated - // before referencing tables. - - logger.Logger.Info("migrating apps...") - if err := migrateTable[App](from, tx); err != nil { - return fmt.Errorf("failed to migrate apps: %w", err) - } - - logger.Logger.Info("migrating app_permissions...") - if err := migrateTable[AppPermission](from, tx); err != nil { - return fmt.Errorf("failed to migrate app_permissions: %w", err) - } - - logger.Logger.Info("migrating request_events...") - if err := migrateTable[RequestEvent](from, tx); err != nil { - return fmt.Errorf("failed to migrate request_events: %w", err) - } - - logger.Logger.Info("migrating response_events...") - if err := migrateTable[ResponseEvent](from, tx); err != nil { - return fmt.Errorf("failed to migrate response_events: %w", err) - } - - logger.Logger.Info("migrating transactions...") - if err := migrateTable[Transaction](from, tx); err != nil { - return fmt.Errorf("failed to migrate transactions: %w", err) - } - - logger.Logger.Info("migrating swaps...") - if err := migrateTable[Swap](from, tx); err != nil { - return fmt.Errorf("failed to migrate swaps: %w", err) - } - - logger.Logger.Info("migrating forwards...") - if err := migrateTable[Forward](from, tx); err != nil { - return fmt.Errorf("failed to migrate forwards: %w", err) - } - - logger.Logger.Info("migrating user_configs...") - if err := migrateTable[UserConfig](from, tx); err != nil { - return fmt.Errorf("failed to migrate user_configs: %w", err) - } - - if to.Dialector.Name() == "postgres" { - logger.Logger.Info("resetting sequences...") - if err := resetSequences(tx); err != nil { - return fmt.Errorf("failed to reset sequences: %w", err) - } - } - - tx.Commit() - if err := tx.Error; err != nil { - return fmt.Errorf("failed to commit transaction: %w", err) - } - - return nil -} - -func migrateTable[T any](from, to *gorm.DB) error { - var data []T - if err := from.Find(&data).Error; err != nil { - return fmt.Errorf("failed to fetch data: %w", err) - } - - if len(data) == 0 { - return nil - } - - // to avoid "failed to migrate transactions: failed to insert data: extended protocol limited to 65535 parameters" - // see https://stackoverflow.com/questions/77372430/extended-protocol-limited-to-65535-parameters-golang-gorm - // max statements is 65535 - // but it's the number of records * columns - // to be safe, using a lower value of 1000. - // this will fail if any table has more than 65 columns, which I doubt we will have - max := 1000 - for i := 0; i < len(data); i += max { - j := min(i+max, len(data)) - - if err := to.Create(data[i:j]).Error; err != nil { - return fmt.Errorf("failed to insert data: %w", err) - } - } - - return nil -} - -func checkSchema(db *gorm.DB) error { - tables, err := listTables(db) - if err != nil { - return fmt.Errorf("failed to list database tables: %w", err) - } - - for _, table := range expectedTables { - if !slices.Contains(tables, table) { - return fmt.Errorf("table missing from the database: %q", table) - } - } - - for _, table := range tables { - if !slices.Contains(expectedTables, table) { - return fmt.Errorf("unexpected table found in the database: %q", table) - } - } - - return nil -} - -func listTables(db *gorm.DB) ([]string, error) { - var query string - - switch db.Dialector.Name() { - case "sqlite": - query = "SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%';" - case "postgres": - query = "SELECT tablename FROM pg_tables WHERE schemaname = 'public';" - default: - return nil, fmt.Errorf("unsupported database: %q", db.Dialector.Name()) - } - - rows, err := db.Raw(query).Rows() - if err != nil { - return nil, fmt.Errorf("failed to query table names: %w", err) - } - defer func() { - if err := rows.Close(); err != nil { - logger.Logger.WithError(err).Error("failed to close rows") - } - }() - - var tables []string - for rows.Next() { - var table string - if err := rows.Scan(&table); err != nil { - return nil, fmt.Errorf("failed to scan table name: %w", err) - } - tables = append(tables, table) - } - - return tables, nil -} - -func resetSequences(db *gorm.DB) error { - type resetReq struct { - table string - seq string - } - - resetReqs := []resetReq{ - {"apps", "apps_2_id_seq"}, - {"app_permissions", "app_permissions_2_id_seq"}, - {"request_events", "request_events_id_seq"}, - {"response_events", "response_events_id_seq"}, - {"transactions", "transactions_id_seq"}, - {"swaps", "swaps_id_seq"}, - {"forwards", "forwards_id_seq"}, - {"user_configs", "user_configs_id_seq"}, - } - - for _, req := range resetReqs { - if err := resetPostgresSequence(db, req.table, req.seq); err != nil { - return fmt.Errorf("failed to reset sequence %q for %q: %w", req.seq, req.table, err) - } - } - - return nil -} - -func resetPostgresSequence(db *gorm.DB, table string, seq string) error { - query := fmt.Sprintf("SELECT setval('%s', (SELECT MAX(id) FROM %s));", seq, table) - if err := db.Exec(query).Error; err != nil { - return fmt.Errorf("failed to execute setval(): %w", err) - } - - return nil -} diff --git a/frontend/package.json b/frontend/package.json index 3109831a..71cc9fa7 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -20,13 +20,14 @@ "prepare": "cd .. && husky frontend/.husky" }, "dependencies": { - "@base-ui/react": "^1.5.0", - "@fontsource-variable/figtree": "^5.3.0", - "@fontsource-variable/inter": "^5.3.0", + "@base-ui/react": "^1.4.1", + "@fontsource-variable/figtree": "^5.2.10", + "@fontsource-variable/inter": "^5.2.8", "@getalby/lightning-tools": "^8.1.0", - "@getalby/sdk": "^8.0.3", + "@getalby/sdk": "^7.0.0", "@scure/bip39": "^2.2.0", "@stepperize/react": "^6.1.0", + "argon2-wasm-esm": "^1.0.3", "bitcoin-address-validation": "^3.0.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", @@ -35,35 +36,36 @@ "date-fns": "^4.1.0", "dayjs": "^1.11.20", "embla-carousel-react": "^8.6.0", - "lottie-react": "^2.4.1", - "lucide-react": "^1.28.0", - "qr-code-styling": "^1.9.2", + "lucide-react": "^1.7.0", "radix-ui": "^1.4.3", "react": "^19.2.6", "react-day-picker": "^9.14.0", "react-dom": "^19.2.6", - "react-router": "^7.18.2", + "react-lottie": "^1.2.4", + "react-qr-code": "^2.0.12", + "react-router": "^7.14.2", "sonner": "^2.0.7", "swr": "^2.4.1", - "tailwind-merge": "^3.6.0", + "tailwind-merge": "^3.4.1", "tw-animate-css": "^1.4.0", "vaul": "^1.1.2", "zustand": "^5.0.12" }, "devDependencies": { "@commitlint/cli": "^20.5.3", - "@commitlint/config-conventional": "^21.2.0", + "@commitlint/config-conventional": "^20.5.0", "@eslint/eslintrc": "^3.3.5", "@eslint/js": "^10.0.1", "@tailwindcss/aspect-ratio": "^0.4.2", - "@tailwindcss/forms": "^0.5.11", + "@tailwindcss/forms": "^0.5.7", "@tailwindcss/typography": "^0.5.19", "@tailwindcss/vite": "^4.2.4", - "@types/node": "^25.9.3", + "@types/node": "^25.6.2", "@types/react": "^19.2.14", "@types/react-dom": "^19.0.0", - "@vitejs/plugin-react-swc": "^4.3.1", - "eslint": "^10.4.1", + "@types/react-lottie": "^1.2.10", + "@vitejs/plugin-react-swc": "^4.3.0", + "eslint": "^10.3.0", "eslint-config-prettier": "^10.1.8", "eslint-plugin-react-hooks": "^7.1.1", "eslint-plugin-react-refresh": "^0.5.2", @@ -72,11 +74,12 @@ "lint-staged": "^16.4.0", "prettier": "3.8.3", "shx": "^0.4.0", - "tailwindcss": "^4.3.0", + "tailwindcss": "^4.2.2", "typescript": "^5.9.3", - "typescript-eslint": "^8.61.0", - "vite": "^8.2.1", - "vite-plugin-pwa": "^1.3.0" + "typescript-eslint": "^8.59.2", + "vite": "^5.4.0", + "vite-plugin-pwa": "^1.2.0", + "vite-tsconfig-paths": "^6.1.1" }, "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e" } diff --git a/frontend/platform_specific/wails/src/utils/request.ts b/frontend/platform_specific/wails/src/utils/request.ts index 4d7d8dfb..aef986bf 100644 --- a/frontend/platform_specific/wails/src/utils/request.ts +++ b/frontend/platform_specific/wails/src/utils/request.ts @@ -10,7 +10,7 @@ export const request = async ( args[1]?.body?.toString() || "" ); - console.info("Wails request", args[0].toString(), args[1]?.method || "GET"); + console.info("Wails request", ...args, res); if (res.error) { throw new Error(res.error); } diff --git a/frontend/src/assets/cards/2fiat.png b/frontend/src/assets/cards/2fiat.png deleted file mode 100644 index dd6b3732..00000000 Binary files a/frontend/src/assets/cards/2fiat.png and /dev/null differ diff --git a/frontend/src/assets/cards/freedomia.png b/frontend/src/assets/cards/freedomia.png deleted file mode 100644 index 2b876b66..00000000 Binary files a/frontend/src/assets/cards/freedomia.png and /dev/null differ diff --git a/frontend/src/assets/cards/redotpay.png b/frontend/src/assets/cards/redotpay.png deleted file mode 100644 index cc72cb4c..00000000 Binary files a/frontend/src/assets/cards/redotpay.png and /dev/null differ diff --git a/frontend/src/assets/cards/solvocard.png b/frontend/src/assets/cards/solvocard.png deleted file mode 100644 index b5a02889..00000000 Binary files a/frontend/src/assets/cards/solvocard.png and /dev/null differ diff --git a/frontend/src/assets/images/node/bark-dark.svg b/frontend/src/assets/images/node/bark-dark.svg deleted file mode 100644 index 8cc83e3a..00000000 --- a/frontend/src/assets/images/node/bark-dark.svg +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - diff --git a/frontend/src/assets/images/node/bark-light.svg b/frontend/src/assets/images/node/bark-light.svg deleted file mode 100644 index 1707c3b5..00000000 --- a/frontend/src/assets/images/node/bark-light.svg +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - diff --git a/frontend/src/assets/lotties/success-check.json b/frontend/src/assets/lotties/success-check.json deleted file mode 100644 index 9eee6036..00000000 --- a/frontend/src/assets/lotties/success-check.json +++ /dev/null @@ -1,8588 +0,0 @@ -{ - "h": 1000, - "w": 1000, - "meta": { - "a": "Muzahid Ahinger", - "k": "check done ok complete checked", - "d": "Check, done, complete animated signs and symbols", - "g": "@lottiefiles/toolkit-js 0.57.1-beta.0", - "tc": "#ffffff" - }, - "layers": [ - { - "ty": 4, - "sr": 1, - "st": 2, - "op": 71, - "ip": 2, - "ln": "14", - "hasMask": false, - "ao": 0, - "ks": { - "a": { "a": 0, "k": [0, 0] }, - "s": { - "a": 1, - "k": [ - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [112.5, 112.5, 100], - "t": 2 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 3 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [164.167, 164.167, 100], - "t": 4 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [175.388, 175.388, 100], - "t": 5 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [183.898, 183.898, 100], - "t": 6 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [189.962, 189.962, 100], - "t": 7 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [193.86, 193.86, 100], - "t": 8 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [195.883, 195.883, 100], - "t": 9 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [196.315, 196.315, 100], - "t": 10 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [195.434, 195.434, 100], - "t": 11 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [193.5, 193.5, 100], - "t": 12 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [190.758, 190.758, 100], - "t": 13 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [187.425, 187.425, 100], - "t": 14 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [183.699, 183.699, 100], - "t": 15 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [179.749, 179.749, 100], - "t": 16 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [175.722, 175.722, 100], - "t": 17 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [171.739, 171.739, 100], - "t": 18 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [167.898, 167.898, 100], - "t": 19 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [164.277, 164.277, 100], - "t": 20 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [160.931, 160.931, 100], - "t": 21 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [157.9, 157.9, 100], - "t": 22 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [155.207, 155.207, 100], - "t": 23 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [152.864, 152.864, 100], - "t": 24 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150.87, 150.87, 100], - "t": 25 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [149.213, 149.213, 100], - "t": 26 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [147.878, 147.878, 100], - "t": 27 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [146.842, 146.842, 100], - "t": 28 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [146.077, 146.077, 100], - "t": 29 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [145.556, 145.556, 100], - "t": 30 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [145.248, 145.248, 100], - "t": 31 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [145.122, 145.122, 100], - "t": 32 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [145.149, 145.149, 100], - "t": 33 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [145.301, 145.301, 100], - "t": 34 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [145.55, 145.55, 100], - "t": 35 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [145.873, 145.873, 100], - "t": 36 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [146.248, 146.248, 100], - "t": 37 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [146.654, 146.654, 100], - "t": 38 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [147.077, 147.077, 100], - "t": 39 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [147.5, 147.5, 100], - "t": 40 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [147.914, 147.914, 100], - "t": 41 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [148.308, 148.308, 100], - "t": 42 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [148.676, 148.676, 100], - "t": 43 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [149.012, 149.012, 100], - "t": 44 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [149.314, 149.314, 100], - "t": 45 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [149.579, 149.579, 100], - "t": 46 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [149.808, 149.808, 100], - "t": 47 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 48 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150.157, 150.157, 100], - "t": 49 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150.282, 150.282, 100], - "t": 50 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150.377, 150.377, 100], - "t": 51 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150.444, 150.444, 100], - "t": 52 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150.487, 150.487, 100], - "t": 53 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150.51, 150.51, 100], - "t": 54 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150.515, 150.515, 100], - "t": 55 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150.505, 150.505, 100], - "t": 56 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150.483, 150.483, 100], - "t": 57 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150.453, 150.453, 100], - "t": 58 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150.416, 150.416, 100], - "t": 59 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150.374, 150.374, 100], - "t": 60 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150.33, 150.33, 100], - "t": 61 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150.286, 150.286, 100], - "t": 62 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150.241, 150.241, 100], - "t": 63 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150.199, 150.199, 100], - "t": 64 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150.159, 150.159, 100], - "t": 65 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150.121, 150.121, 100], - "t": 66 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150.088, 150.088, 100], - "t": 67 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150.058, 150.058, 100], - "t": 68 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150.032, 150.032, 100], - "t": 69 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150.01, 150.01, 100], - "t": 70 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [149.991, 149.991, 100], - "t": 71 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [149.976, 149.976, 100], - "t": 72 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [149.965, 149.965, 100], - "t": 73 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [149.956, 149.956, 100], - "t": 74 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [149.951, 149.951, 100], - "t": 75 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [149.947, 149.947, 100], - "t": 76 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [149.946, 149.946, 100], - "t": 77 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [149.946, 149.946, 100], - "t": 78 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [149.948, 149.948, 100], - "t": 79 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [149.951, 149.951, 100], - "t": 80 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [149.954, 149.954, 100], - "t": 81 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [149.958, 149.958, 100], - "t": 82 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [149.963, 149.963, 100], - "t": 83 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [149.968, 149.968, 100], - "t": 84 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [149.972, 149.972, 100], - "t": 85 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [149.977, 149.977, 100], - "t": 86 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [149.981, 149.981, 100], - "t": 87 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [149.985, 149.985, 100], - "t": 88 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [149.989, 149.989, 100], - "t": 89 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [149.992, 149.992, 100], - "t": 90 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [149.995, 149.995, 100], - "t": 91 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [149.998, 149.998, 100], - "t": 92 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 93 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150.002, 150.002, 100], - "t": 94 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150.003, 150.003, 100], - "t": 95 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150.004, 150.004, 100], - "t": 96 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150.005, 150.005, 100], - "t": 97 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150.005, 150.005, 100], - "t": 98 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150.006, 150.006, 100], - "t": 99 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150.006, 150.006, 100], - "t": 100 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150.006, 150.006, 100], - "t": 101 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150.005, 150.005, 100], - "t": 102 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150.005, 150.005, 100], - "t": 103 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150.005, 150.005, 100], - "t": 104 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150.004, 150.004, 100], - "t": 105 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150.004, 150.004, 100], - "t": 106 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150.003, 150.003, 100], - "t": 107 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150.003, 150.003, 100], - "t": 108 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150.002, 150.002, 100], - "t": 109 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150.002, 150.002, 100], - "t": 110 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150.001, 150.001, 100], - "t": 111 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150.001, 150.001, 100], - "t": 112 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150.001, 150.001, 100], - "t": 113 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 114 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 115 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 116 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 117 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 118 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 119 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [149.999, 149.999, 100], - "t": 120 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [149.999, 149.999, 100], - "t": 121 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [149.999, 149.999, 100], - "t": 122 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [149.999, 149.999, 100], - "t": 123 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [149.999, 149.999, 100], - "t": 124 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [149.999, 149.999, 100], - "t": 125 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [149.999, 149.999, 100], - "t": 126 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 127 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 128 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 129 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 130 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 131 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 132 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 133 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 134 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 135 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 136 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 137 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 138 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 139 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 140 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 141 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 142 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 143 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 144 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 145 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 146 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 147 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 148 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 149 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 150 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 151 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 152 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 153 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 154 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 155 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 156 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 157 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 158 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 159 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 160 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 161 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 162 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 163 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 164 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 165 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 166 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 167 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 168 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 169 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 170 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 171 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 172 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 173 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 174 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 175 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 176 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 177 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 178 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 179 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 180 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 181 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 182 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 183 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 184 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 185 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 186 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 187 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 188 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 189 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 190 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 191 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 192 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 193 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 194 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 195 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 196 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 197 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 198 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 199 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 200 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 201 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 202 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 203 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 204 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 205 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 206 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 207 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 208 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 209 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 210 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 211 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 212 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 213 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 214 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 215 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 216 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 217 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 218 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 219 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 220 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 221 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 222 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 223 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 224 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 225 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 226 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 227 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 228 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 229 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 230 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 231 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 232 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 233 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 234 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 235 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 236 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 237 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 238 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 239 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 240 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 241 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 242 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 243 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 244 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 245 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 246 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 247 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 248 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 249 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 250 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 251 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 252 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 253 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 254 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 255 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 256 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 257 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 258 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 259 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 260 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 261 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 262 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 263 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 264 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 265 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 266 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 267 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 268 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 269 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 270 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 271 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 272 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 273 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 274 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 275 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 276 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 277 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 278 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 279 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 280 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 281 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 282 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 283 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 284 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 285 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 286 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 287 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 288 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 289 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 290 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 291 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 292 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 293 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 294 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 295 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 296 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 297 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 298 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 299 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 300 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 301 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 302 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 303 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 304 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 305 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 306 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 307 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 308 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 309 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 310 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 311 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 312 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 313 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 314 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 315 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 316 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 317 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 318 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 319 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 320 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 321 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 322 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 323 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 324 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 325 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 326 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 327 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 328 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 329 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 330 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 331 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 332 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 333 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 334 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 335 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 336 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 337 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 338 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 339 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 340 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 341 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 342 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 343 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 344 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 345 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 346 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 347 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 348 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 349 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 350 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 351 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 352 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 353 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 354 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 355 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 356 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 357 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 358 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 359 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 360 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 361 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 362 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 363 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 364 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 365 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 366 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 367 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 368 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 369 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 370 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 371 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 372 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 373 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 374 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 375 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 376 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 377 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 378 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 379 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 380 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 381 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 382 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 383 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 384 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 385 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 386 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 387 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 388 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 389 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 390 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 391 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 392 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 393 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 394 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 395 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 396 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 397 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 398 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 399 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 400 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 401 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 402 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 403 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 404 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 405 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 406 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 407 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 408 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 409 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 410 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 411 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 412 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 413 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 414 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 415 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 416 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 417 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 418 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 419 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 420 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 421 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 422 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 423 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 424 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 425 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 426 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 427 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 428 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 429 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 430 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 431 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 432 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 433 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 434 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 435 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 436 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 437 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 438 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 439 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 440 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 441 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 442 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 443 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 444 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 445 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 446 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 447 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 448 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 449 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 450 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 451 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 452 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 453 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 454 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 455 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 456 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 457 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 458 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 459 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 460 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 461 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 462 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 463 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 464 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 465 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 466 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 467 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 468 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 469 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 470 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 471 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 472 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 473 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 474 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 475 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 476 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 477 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 478 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 479 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 480 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 481 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 482 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 483 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 484 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 485 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 486 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 487 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 488 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 489 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 490 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 491 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 492 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 493 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 494 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 495 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 496 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 497 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 498 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 499 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 500 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 501 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 502 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 503 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 504 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 505 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 506 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 507 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 508 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 509 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 510 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 511 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 512 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 513 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 514 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 515 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 516 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 517 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 518 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 519 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 520 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 521 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 522 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 523 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 524 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 525 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 526 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 527 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 528 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 529 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 530 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 531 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 532 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 533 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 534 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 535 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 536 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 537 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 538 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 539 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 540 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 541 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 542 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 543 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 544 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 545 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 546 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 547 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 548 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 549 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 550 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 551 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 552 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 553 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 554 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 555 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 556 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 557 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 558 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 559 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 560 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 561 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 562 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 563 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 564 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 565 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 566 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 567 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 568 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 569 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 570 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 571 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 572 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 573 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 574 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 575 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 576 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 577 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 578 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 579 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 580 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 581 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 582 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 583 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 584 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 585 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 586 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 587 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 588 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 589 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 590 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 591 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 592 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 593 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 594 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 595 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 596 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 597 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 598 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 599 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 600 - }, - { "s": [150, 150, 100], "t": 601 } - ] - }, - "p": { "a": 0, "k": [491, 480.676, 0] }, - "r": { "a": 0, "k": 0 }, - "o": { "a": 0, "k": 100 } - }, - "shapes": [ - { - "ty": "gr", - "it": [ - { - "ty": "sh", - "d": 1, - "ks": { - "a": 0, - "k": { - "c": false, - "i": [ - [0, 0], - [0, 0], - [0, 0] - ], - "o": [ - [0, 0], - [0, 0], - [0, 0] - ], - "v": [ - [-85.202, 28.7], - [-32.001, 83.001], - [90.251, -41.301] - ] - } - } - }, - { - "ty": "tr", - "a": { "a": 0, "k": [0, 0] }, - "s": { "a": 0, "k": [100, 100] }, - "p": { "a": 0, "k": [0, 0] }, - "r": { "a": 0, "k": 0 }, - "o": { "a": 0, "k": 100 } - } - ] - }, - { - "ty": "tm", - "e": { - "a": 1, - "k": [ - { - "o": { "x": 0.333, "y": 0 }, - "i": { "x": 0.667, "y": 1 }, - "s": [0], - "t": 6 - }, - { "s": [100], "t": 16 } - ] - }, - "o": { "a": 0, "k": 0 }, - "s": { "a": 0, "k": 0 }, - "m": 1 - }, - { - "ty": "st", - "lc": 2, - "lj": 2, - "ml": 4, - "o": { "a": 0, "k": 100 }, - "w": { "a": 0, "k": 40 }, - "c": { "a": 0, "k": [1, 1, 1] } - } - ], - "ind": 1 - }, - { - "ty": 4, - "sr": 1, - "st": 1, - "op": 71, - "ip": 0, - "ln": "13", - "hasMask": false, - "ao": 0, - "ks": { - "a": { "a": 0, "k": [0, 0] }, - "s": { - "a": 1, - "k": [ - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [90, 90, 100], - "t": 0 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [120, 120, 100], - "t": 1 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 2 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [161.334, 161.334, 100], - "t": 3 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [170.311, 170.311, 100], - "t": 4 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [177.119, 177.119, 100], - "t": 5 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [181.969, 181.969, 100], - "t": 6 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [185.088, 185.088, 100], - "t": 7 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [186.706, 186.706, 100], - "t": 8 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [187.052, 187.052, 100], - "t": 9 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [186.347, 186.347, 100], - "t": 10 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [184.8, 184.8, 100], - "t": 11 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [182.606, 182.606, 100], - "t": 12 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [179.94, 179.94, 100], - "t": 13 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [176.959, 176.959, 100], - "t": 14 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [173.799, 173.799, 100], - "t": 15 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [170.578, 170.578, 100], - "t": 16 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [167.391, 167.391, 100], - "t": 17 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [164.319, 164.319, 100], - "t": 18 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [161.421, 161.421, 100], - "t": 19 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [158.744, 158.744, 100], - "t": 20 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [156.32, 156.32, 100], - "t": 21 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [154.166, 154.166, 100], - "t": 22 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [152.291, 152.291, 100], - "t": 23 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150.696, 150.696, 100], - "t": 24 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [149.371, 149.371, 100], - "t": 25 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [148.302, 148.302, 100], - "t": 26 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [147.473, 147.473, 100], - "t": 27 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [146.862, 146.862, 100], - "t": 28 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [146.445, 146.445, 100], - "t": 29 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [146.198, 146.198, 100], - "t": 30 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [146.098, 146.098, 100], - "t": 31 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [146.119, 146.119, 100], - "t": 32 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [146.241, 146.241, 100], - "t": 33 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [146.44, 146.44, 100], - "t": 34 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [146.699, 146.699, 100], - "t": 35 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [146.998, 146.998, 100], - "t": 36 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [147.324, 147.324, 100], - "t": 37 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [147.661, 147.661, 100], - "t": 38 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [148, 148, 100], - "t": 39 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [148.331, 148.331, 100], - "t": 40 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [148.646, 148.646, 100], - "t": 41 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [148.94, 148.94, 100], - "t": 42 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [149.21, 149.21, 100], - "t": 43 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [149.451, 149.451, 100], - "t": 44 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [149.663, 149.663, 100], - "t": 45 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [149.846, 149.846, 100], - "t": 46 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 47 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150.126, 150.126, 100], - "t": 48 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150.226, 150.226, 100], - "t": 49 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150.301, 150.301, 100], - "t": 50 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150.355, 150.355, 100], - "t": 51 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150.39, 150.39, 100], - "t": 52 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150.408, 150.408, 100], - "t": 53 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150.412, 150.412, 100], - "t": 54 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150.404, 150.404, 100], - "t": 55 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150.387, 150.387, 100], - "t": 56 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150.362, 150.362, 100], - "t": 57 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150.333, 150.333, 100], - "t": 58 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150.299, 150.299, 100], - "t": 59 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150.264, 150.264, 100], - "t": 60 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150.229, 150.229, 100], - "t": 61 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150.193, 150.193, 100], - "t": 62 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150.159, 150.159, 100], - "t": 63 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150.127, 150.127, 100], - "t": 64 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150.097, 150.097, 100], - "t": 65 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150.07, 150.07, 100], - "t": 66 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150.046, 150.046, 100], - "t": 67 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150.025, 150.025, 100], - "t": 68 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150.008, 150.008, 100], - "t": 69 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [149.993, 149.993, 100], - "t": 70 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [149.981, 149.981, 100], - "t": 71 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [149.972, 149.972, 100], - "t": 72 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [149.965, 149.965, 100], - "t": 73 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [149.961, 149.961, 100], - "t": 74 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [149.958, 149.958, 100], - "t": 75 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [149.957, 149.957, 100], - "t": 76 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [149.957, 149.957, 100], - "t": 77 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [149.958, 149.958, 100], - "t": 78 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [149.96, 149.96, 100], - "t": 79 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [149.963, 149.963, 100], - "t": 80 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [149.967, 149.967, 100], - "t": 81 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [149.97, 149.97, 100], - "t": 82 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [149.974, 149.974, 100], - "t": 83 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [149.978, 149.978, 100], - "t": 84 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [149.981, 149.981, 100], - "t": 85 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [149.985, 149.985, 100], - "t": 86 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [149.988, 149.988, 100], - "t": 87 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [149.991, 149.991, 100], - "t": 88 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [149.994, 149.994, 100], - "t": 89 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [149.996, 149.996, 100], - "t": 90 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [149.998, 149.998, 100], - "t": 91 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 92 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150.001, 150.001, 100], - "t": 93 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150.003, 150.003, 100], - "t": 94 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150.003, 150.003, 100], - "t": 95 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150.004, 150.004, 100], - "t": 96 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150.004, 150.004, 100], - "t": 97 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150.005, 150.005, 100], - "t": 98 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150.005, 150.005, 100], - "t": 99 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150.004, 150.004, 100], - "t": 100 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150.004, 150.004, 100], - "t": 101 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150.004, 150.004, 100], - "t": 102 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150.004, 150.004, 100], - "t": 103 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150.003, 150.003, 100], - "t": 104 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150.003, 150.003, 100], - "t": 105 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150.003, 150.003, 100], - "t": 106 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150.002, 150.002, 100], - "t": 107 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150.002, 150.002, 100], - "t": 108 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150.001, 150.001, 100], - "t": 109 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150.001, 150.001, 100], - "t": 110 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150.001, 150.001, 100], - "t": 111 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150.001, 150.001, 100], - "t": 112 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 113 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 114 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 115 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 116 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 117 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 118 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 119 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 120 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 121 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 122 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 123 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 124 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 125 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 126 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 127 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 128 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 129 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 130 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 131 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 132 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 133 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 134 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 135 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 136 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 137 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 138 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 139 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 140 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 141 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 142 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 143 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 144 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 145 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 146 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 147 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 148 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 149 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 150 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 151 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 152 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 153 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 154 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 155 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 156 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 157 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 158 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 159 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 160 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 161 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 162 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 163 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 164 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 165 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 166 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 167 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 168 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 169 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 170 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 171 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 172 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 173 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 174 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 175 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 176 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 177 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 178 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 179 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 180 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 181 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 182 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 183 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 184 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 185 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 186 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 187 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 188 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 189 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 190 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 191 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 192 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 193 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 194 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 195 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 196 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 197 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 198 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 199 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 200 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 201 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 202 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 203 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 204 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 205 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 206 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 207 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 208 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 209 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 210 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 211 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 212 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 213 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 214 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 215 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 216 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 217 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 218 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 219 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 220 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 221 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 222 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 223 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 224 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 225 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 226 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 227 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 228 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 229 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 230 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 231 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 232 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 233 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 234 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 235 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 236 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 237 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 238 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 239 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 240 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 241 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 242 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 243 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 244 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 245 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 246 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 247 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 248 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 249 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 250 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 251 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 252 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 253 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 254 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 255 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 256 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 257 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 258 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 259 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 260 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 261 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 262 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 263 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 264 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 265 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 266 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 267 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 268 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 269 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 270 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 271 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 272 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 273 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 274 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 275 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 276 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 277 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 278 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 279 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 280 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 281 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 282 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 283 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 284 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 285 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 286 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 287 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 288 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 289 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 290 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 291 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 292 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 293 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 294 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 295 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 296 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 297 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 298 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 299 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 300 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 301 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 302 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 303 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 304 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 305 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 306 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 307 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 308 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 309 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 310 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 311 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 312 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 313 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 314 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 315 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 316 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 317 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 318 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 319 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 320 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 321 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 322 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 323 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 324 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 325 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 326 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 327 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 328 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 329 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 330 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 331 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 332 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 333 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 334 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 335 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 336 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 337 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 338 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 339 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 340 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 341 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 342 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 343 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 344 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 345 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 346 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 347 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 348 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 349 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 350 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 351 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 352 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 353 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 354 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 355 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 356 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 357 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 358 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 359 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 360 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 361 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 362 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 363 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 364 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 365 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 366 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 367 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 368 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 369 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 370 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 371 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 372 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 373 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 374 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 375 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 376 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 377 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 378 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 379 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 380 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 381 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 382 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 383 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 384 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 385 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 386 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 387 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 388 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 389 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 390 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 391 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 392 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 393 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 394 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 395 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 396 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 397 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 398 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 399 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 400 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 401 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 402 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 403 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 404 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 405 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 406 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 407 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 408 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 409 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 410 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 411 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 412 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 413 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 414 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 415 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 416 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 417 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 418 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 419 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 420 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 421 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 422 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 423 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 424 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 425 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 426 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 427 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 428 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 429 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 430 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 431 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 432 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 433 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 434 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 435 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 436 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 437 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 438 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 439 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 440 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 441 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 442 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 443 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 444 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 445 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 446 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 447 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 448 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 449 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 450 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 451 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 452 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 453 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 454 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 455 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 456 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 457 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 458 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 459 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 460 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 461 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 462 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 463 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 464 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 465 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 466 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 467 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 468 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 469 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 470 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 471 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 472 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 473 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 474 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 475 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 476 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 477 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 478 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 479 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 480 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 481 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 482 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 483 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 484 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 485 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 486 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 487 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 488 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 489 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 490 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 491 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 492 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 493 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 494 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 495 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 496 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 497 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 498 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 499 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 500 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 501 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 502 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 503 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 504 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 505 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 506 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 507 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 508 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 509 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 510 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 511 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 512 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 513 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 514 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 515 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 516 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 517 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 518 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 519 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 520 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 521 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 522 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 523 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 524 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 525 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 526 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 527 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 528 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 529 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 530 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 531 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 532 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 533 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 534 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 535 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 536 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 537 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 538 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 539 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 540 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 541 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 542 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 543 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 544 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 545 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 546 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 547 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 548 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 549 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 550 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 551 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 552 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 553 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 554 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 555 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 556 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 557 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 558 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 559 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 560 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 561 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 562 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 563 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 564 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 565 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 566 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 567 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 568 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 569 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 570 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 571 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 572 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 573 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 574 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 575 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 576 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 577 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 578 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 579 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 580 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 581 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 582 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 583 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 584 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 585 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 586 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 587 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 588 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 589 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 590 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 591 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 592 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 593 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 594 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 595 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 596 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 597 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 598 - }, - { - "h": 1, - "o": { "x": 0.167, "y": 0.167 }, - "i": { "x": 0.833, "y": 0.833 }, - "s": [150, 150, 100], - "t": 599 - }, - { "s": [150, 150, 100], "t": 600 } - ] - }, - "p": { "a": 0, "k": [500, 500] }, - "r": { "a": 0, "k": 0 }, - "o": { "a": 0, "k": 100 } - }, - "shapes": [ - { - "ty": "gr", - "it": [ - { - "ty": "sh", - "d": 1, - "ks": { - "a": 0, - "k": { - "c": true, - "i": [ - [-110.457, 0], - [0, -110.457], - [110.457, 0], - [0, 110.457] - ], - "o": [ - [110.457, 0], - [0, 110.457], - [-110.457, 0], - [0, -110.457] - ], - "v": [ - [0, -200], - [200, 0], - [0, 200], - [-200, 0] - ] - } - } - }, - { - "ty": "st", - "lc": 1, - "lj": 1, - "ml": 4, - "o": { "a": 0, "k": 100 }, - "w": { "a": 0, "k": 0 }, - "c": { "a": 0, "k": [0.596, 0.875, 0.839] } - }, - { - "ty": "fl", - "c": { "a": 0, "k": [0.296, 0.698, 0] }, - "r": 1, - "o": { "a": 0, "k": 0 } - }, - { - "ty": "tr", - "a": { "a": 0, "k": [0, 0] }, - "s": { "a": 0, "k": [100, 100] }, - "p": { "a": 0, "k": [0, 0] }, - "r": { "a": 0, "k": 0 }, - "o": { "a": 0, "k": 100 } - } - ] - }, - { - "ty": "fl", - "c": { "a": 0, "k": [0.296, 0.698, 0] }, - "r": 1, - "o": { "a": 0, "k": 100 } - } - ], - "ind": 2 - } - ], - "v": "5.7.0", - "fr": 60, - "op": 71, - "ip": 0, - "assets": [] -} diff --git a/frontend/src/assets/suggested-apps/bitcoin-card-topup.png b/frontend/src/assets/suggested-apps/bitcoin-card-topup.png deleted file mode 100644 index 3ca4037d..00000000 Binary files a/frontend/src/assets/suggested-apps/bitcoin-card-topup.png and /dev/null differ diff --git a/frontend/src/assets/suggested-apps/hermes.png b/frontend/src/assets/suggested-apps/hermes.png deleted file mode 100644 index dc8a6ec6..00000000 Binary files a/frontend/src/assets/suggested-apps/hermes.png and /dev/null differ diff --git a/frontend/src/assets/suggested-apps/lendaswap.png b/frontend/src/assets/suggested-apps/lendaswap.png new file mode 100644 index 00000000..6987c6ac Binary files /dev/null and b/frontend/src/assets/suggested-apps/lendaswap.png differ diff --git a/frontend/src/assets/suggested-apps/paper-scissors-hodl.png b/frontend/src/assets/suggested-apps/paper-scissors-hodl.png new file mode 100644 index 00000000..4fe1fd0c Binary files /dev/null and b/frontend/src/assets/suggested-apps/paper-scissors-hodl.png differ diff --git a/frontend/src/assets/suggested-apps/pi.png b/frontend/src/assets/suggested-apps/pi.png deleted file mode 100644 index dbf58268..00000000 Binary files a/frontend/src/assets/suggested-apps/pi.png and /dev/null differ diff --git a/frontend/src/assets/suggested-apps/satora.png b/frontend/src/assets/suggested-apps/satora.png deleted file mode 100644 index f94f9ab5..00000000 Binary files a/frontend/src/assets/suggested-apps/satora.png and /dev/null differ diff --git a/frontend/src/assets/suggested-apps/satoshis-auction-house.png b/frontend/src/assets/suggested-apps/satoshis-auction-house.png new file mode 100644 index 00000000..f132c37e Binary files /dev/null and b/frontend/src/assets/suggested-apps/satoshis-auction-house.png differ diff --git a/frontend/src/components/AnchorReserveAlert.tsx b/frontend/src/components/AnchorReserveAlert.tsx index 0cf5983f..a4aa1ee5 100644 --- a/frontend/src/components/AnchorReserveAlert.tsx +++ b/frontend/src/components/AnchorReserveAlert.tsx @@ -32,14 +32,12 @@ export function AnchorReserveAlert({ Channel Anchor Reserves will be depleted -

- You have channels open and by spending your entire on-chain balance - including your anchor reserves may put your node at risk of unable to - reclaim funds in your channel after a force-closure. To prevent this, - set aside at least{" "} - {" "} - on-chain. -

+ You have channels open and by spending your entire on-chain balance + including your anchor reserves may put your node at risk of unable to + reclaim funds in your channel after a force-closure. To prevent this, + set aside at least{" "} + {" "} + on-chain.
); diff --git a/frontend/src/components/AppAvatar.tsx b/frontend/src/components/AppAvatar.tsx index cd658007..526c1be4 100644 --- a/frontend/src/components/AppAvatar.tsx +++ b/frontend/src/components/AppAvatar.tsx @@ -4,7 +4,6 @@ import codexLogo from "src/assets/suggested-apps/codex.png"; import cursorLogo from "src/assets/suggested-apps/cursor.png"; import geminiLogo from "src/assets/suggested-apps/gemini.png"; import gooseLogo from "src/assets/suggested-apps/goose.png"; -import hermesLogo from "src/assets/suggested-apps/hermes.png"; import openclawLogo from "src/assets/suggested-apps/openclaw.png"; import opencodeLogo from "src/assets/suggested-apps/opencode.png"; import { appStoreApps } from "src/components/connections/SuggestedAppData"; @@ -18,7 +17,6 @@ import { App } from "src/types"; const agentLogos: Record = { claude: claudeLogo, goose: gooseLogo, - hermes: hermesLogo, openclaw: openclawLogo, cursor: cursorLogo, codex: codexLogo, diff --git a/frontend/src/components/AppSidebar.tsx b/frontend/src/components/AppSidebar.tsx index 3157a289..6d4eb8e8 100644 --- a/frontend/src/components/AppSidebar.tsx +++ b/frontend/src/components/AppSidebar.tsx @@ -2,7 +2,6 @@ import { BotIcon, BoxIcon, ChevronsUpDownIcon, - CreditCardIcon, CircleHelpIcon, HandCoinsIcon, HomeIcon, @@ -106,11 +105,6 @@ export function AppSidebar() { title: "AI & Agents", url: "/ai", icon: BotIcon, - }, - { - title: "Cards", - url: "/cards", - icon: CreditCardIcon, badge: "NEW", }, ], diff --git a/frontend/src/components/CurrencyInputField.tsx b/frontend/src/components/CurrencyInputField.tsx deleted file mode 100644 index 33014d91..00000000 --- a/frontend/src/components/CurrencyInputField.tsx +++ /dev/null @@ -1,528 +0,0 @@ -import * as React from "react"; -import { toast } from "sonner"; -import { - Field, - FieldDescription, - FieldError, - FieldLabel, -} from "src/components/ui/field"; -import { - InputGroup, - InputGroupAddon, - InputGroupButton, - InputGroupInput, -} from "src/components/ui/input-group"; -import { Skeleton } from "src/components/ui/skeleton"; -import { BITCOIN_DISPLAY_FORMAT_BIP177 } from "src/constants"; -import { useBitcoinRate } from "src/hooks/useBitcoinRate"; -import { useInfo } from "src/hooks/useInfo"; -import { cn } from "src/lib/utils"; - -type CurrencyInputMode = "bitcoin" | "fiat"; -type BitcoinDenomination = "sats" | "btc"; - -export type CurrencyInputContextRow = { - label: string; - amountSat?: number | null; - value?: React.ReactNode; -}; - -type CurrencyInputFieldProps = Omit< - React.ComponentProps, - "max" | "min" | "onChange" | "step" | "type" | "value" -> & { - contextRows?: CurrencyInputContextRow[]; - description?: React.ReactNode; - error?: React.ReactNode; - label?: React.ReactNode; - maxSat?: number; - minSat?: number; - onValueSatChange: (valueSat: string) => void; - valueSat: string; -}; - -const SATS_PER_BTC = 100_000_000; - -function getNumericValue(value: string | number | null | undefined) { - const parsed = Number(value); - return Number.isFinite(parsed) ? parsed : 0; -} - -function getCurrencyFractionDigits(currency: string) { - try { - return new Intl.NumberFormat("en-US", { - currency, - style: "currency", - }).resolvedOptions().maximumFractionDigits; - } catch { - return 2; - } -} - -function getCurrencySymbol(currency: string) { - try { - return ( - new Intl.NumberFormat("en-US", { - currency, - style: "currency", - }) - .formatToParts(0) - .find((part) => part.type === "currency")?.value || currency - ); - } catch { - return currency; - } -} - -function formatFiatValue( - amountSat: string | number | undefined, - rate: number | undefined, - currency: string | undefined -) { - if (!rate || !currency) { - return null; - } - - return new Intl.NumberFormat("en-US", { - currency, - style: "currency", - }).format((getNumericValue(amountSat) / SATS_PER_BTC) * rate); -} - -function formatFiatInput(amountSat: string, rate: number, currency: string) { - const fractionDigits = getCurrencyFractionDigits(currency); - const amountFiat = (getNumericValue(amountSat) / SATS_PER_BTC) * rate; - - if (!amountFiat) { - return ""; - } - - return amountFiat.toFixed(fractionDigits); -} - -function formatBitcoinValue( - amountSat: string | number | null | undefined, - displayFormat: string | undefined, - denomination: BitcoinDenomination = "sats" -) { - const { amount, unit } = formatBitcoinValueParts( - amountSat, - displayFormat, - denomination - ); - - if (unit === "₿") { - return `${unit}${amount}`; - } - - return `${amount} ${unit}`; -} - -function formatBitcoinValueParts( - amountSat: string | number | null | undefined, - displayFormat: string | undefined, - denomination: BitcoinDenomination = "sats" -) { - if (denomination === "btc") { - return { - amount: formatBtcDisplay(amountSat), - unit: "BTC", - }; - } - - const formattedAmount = new Intl.NumberFormat().format( - Math.floor(getNumericValue(amountSat)) - ); - - if (displayFormat === BITCOIN_DISPLAY_FORMAT_BIP177) { - return { - amount: formattedAmount, - unit: "₿", - }; - } - - return { - amount: formattedAmount, - unit: "sats", - }; -} - -function BitcoinValueText({ - amountSat, - denomination, - displayFormat, -}: { - amountSat: string | number | null | undefined; - denomination: BitcoinDenomination; - displayFormat: string | undefined; -}) { - const { amount, unit } = formatBitcoinValueParts( - amountSat, - displayFormat, - denomination - ); - - return ( - - {unit === "₿" && {unit}} - {amount} - {unit !== "₿" && {unit}} - - ); -} - -function formatBtcDisplay(amountSat: string | number | null | undefined) { - return (getNumericValue(amountSat) / SATS_PER_BTC).toFixed(8); -} - -function formatBtcInput(amountSat: string | number | null | undefined) { - const amount = getNumericValue(amountSat); - - if (!amount) { - return ""; - } - - return (amount / SATS_PER_BTC).toFixed(8); -} - -export function CurrencyInputField({ - className, - contextRows, - description, - disabled, - error, - id, - label = "Amount", - maxSat, - minSat, - onValueSatChange, - required, - valueSat, - ...props -}: CurrencyInputFieldProps) { - const generatedId = React.useId(); - const { data: info } = useInfo(); - const { data: bitcoinRate, error: bitcoinRateError } = useBitcoinRate( - info?.currency - ); - const [mode, setMode] = React.useState("bitcoin"); - const [fiatValue, setFiatValue] = React.useState(""); - const [bitcoinDenomination, setBitcoinDenomination] = - React.useState("sats"); - const [btcValue, setBtcValue] = React.useState(""); - - const currency = info?.currency || "USD"; - const rate = bitcoinRate?.rate_float; - const canUseFiat = currency !== "SATS" && !!rate && !bitcoinRateError; - const bitcoinUnit = - info?.bitcoinDisplayFormat === BITCOIN_DISPLAY_FORMAT_BIP177 ? "₿" : "sats"; - const invalid = - props["aria-invalid"] === true || - props["aria-invalid"] === "true" || - !!error; - const inputId = id || generatedId; - const isFiatMode = mode === "fiat"; - const isBtcDenominated = bitcoinDenomination === "btc"; - const inputValue = isFiatMode - ? fiatValue - : isBtcDenominated - ? btcValue - : valueSat; - const alternateBitcoinValue = formatBitcoinValueParts( - valueSat, - info?.bitcoinDisplayFormat, - bitcoinDenomination - ); - const alternateValue = isFiatMode - ? formatBitcoinValue( - valueSat, - info?.bitcoinDisplayFormat, - bitcoinDenomination - ) - : formatFiatValue(valueSat, rate, currency); - - React.useEffect(() => { - if (mode === "fiat" && !valueSat) { - setFiatValue(""); - } - }, [mode, valueSat]); - - React.useEffect(() => { - if (mode === "bitcoin" && isBtcDenominated && !valueSat) { - setBtcValue(""); - } - }, [isBtcDenominated, mode, valueSat]); - - function handleToggleMode() { - if (disabled) { - return; - } - - if (mode === "bitcoin") { - if (!canUseFiat) { - return; - } - - setFiatValue(formatFiatInput(valueSat, rate, currency)); - setMode("fiat"); - return; - } - - if (isBtcDenominated) { - setBtcValue(formatBtcInput(valueSat)); - } - - setMode("bitcoin"); - } - - function handleAlternateValueClick() { - if (disabled || isFiatMode || !canUseFiat) { - return; - } - - handleToggleMode(); - } - - function handleToggleBitcoinDenomination() { - if (disabled) { - return; - } - - if (isBtcDenominated) { - setBitcoinDenomination("sats"); - return; - } - - setBtcValue(formatBtcInput(valueSat)); - setBitcoinDenomination("btc"); - } - - function handleChangeMode(event: React.ChangeEvent) { - // clear any custom validity set via onInvalid so the field re-validates - // on the next submit - event.currentTarget.setCustomValidity(""); - - const nextValue = event.target.value.trim(); - - if (mode === "bitcoin") { - if (!isBtcDenominated && nextValue.includes(".")) { - setBitcoinDenomination("btc"); - setBtcValue(nextValue); - toast("Switched to BTC for decimal amount"); - - if (!nextValue) { - onValueSatChange(""); - return; - } - - const amountBtc = Number(nextValue); - if (!Number.isFinite(amountBtc)) { - onValueSatChange(""); - return; - } - - onValueSatChange( - Math.max(0, Math.round(amountBtc * SATS_PER_BTC)).toString() - ); - return; - } - - if (isBtcDenominated) { - setBtcValue(nextValue); - - if (!nextValue) { - onValueSatChange(""); - return; - } - - const amountBtc = Number(nextValue); - if (!Number.isFinite(amountBtc)) { - onValueSatChange(""); - return; - } - - onValueSatChange( - Math.max(0, Math.round(amountBtc * SATS_PER_BTC)).toString() - ); - return; - } - - onValueSatChange(nextValue); - return; - } - - setFiatValue(nextValue); - - if (!nextValue || !rate) { - onValueSatChange(""); - return; - } - - const amountFiat = Number(nextValue); - if (!Number.isFinite(amountFiat)) { - onValueSatChange(""); - return; - } - - onValueSatChange( - Math.max(0, Math.round((amountFiat / rate) * SATS_PER_BTC)).toString() - ); - } - - function getModeBound(amountSat: number | undefined) { - if (amountSat === undefined) { - return undefined; - } - - if (!isFiatMode) { - if (isBtcDenominated) { - return amountSat / SATS_PER_BTC; - } - - return amountSat; - } - - if (!rate) { - return amountSat; - } - - return ((amountSat / SATS_PER_BTC) * rate).toFixed( - getCurrencyFractionDigits(currency) - ); - } - - return ( - - {label && {label}} - - - - {isFiatMode ? ( - - {getCurrencySymbol(currency)} - - ) : ( - - {isBtcDenominated ? "BTC" : bitcoinUnit} - - )} - - - {isFiatMode ? ( - - {alternateBitcoinValue.unit === "₿" && ( - {alternateBitcoinValue.unit} - )} - - {alternateBitcoinValue.amount} - - {alternateBitcoinValue.unit !== "₿" && ( - {alternateBitcoinValue.unit} - )} - - ) : ( - - {alternateValue ?? } - - )} - - - {!!contextRows?.length && ( -
- {contextRows.map((row) => ( -
- {row.label}: - - {row.value ?? ( - - )} - -
- ))} -
- )} - {description && {description}} - {error && {error}} -
- ); -} diff --git a/frontend/src/components/EmptyState.tsx b/frontend/src/components/EmptyState.tsx index 829705b2..9604399c 100644 --- a/frontend/src/components/EmptyState.tsx +++ b/frontend/src/components/EmptyState.tsx @@ -3,44 +3,37 @@ import React from "react"; import { LinkButton } from "src/components/ui/custom/link-button"; import { cn } from "src/lib/utils"; -type Variant = "dashed" | "muted" | "none"; - -type Props = { +interface Props { icon: LucideIcon; title: string; description: string; - variant?: Variant; -} & ( - | { buttonText: string; buttonLink: string } - | { buttonText?: never; buttonLink?: never } -); - -const variantClasses: Record = { - dashed: "shadow-xs border border-dashed", - muted: "bg-muted", - none: "", -}; + buttonText: string; + buttonLink: string; + showButton?: boolean; + showBorder?: boolean; +} const EmptyState: React.FC = ({ icon: Icon, title: message, description: subMessage, - variant = "muted", buttonText, buttonLink, + showButton = true, + showBorder = true, }) => { return (

{message}

{subMessage}

- {buttonText && buttonLink && ( + {showButton && ( {buttonText} diff --git a/frontend/src/components/FirstChannelJitAlert.tsx b/frontend/src/components/FirstChannelJitAlert.tsx deleted file mode 100644 index 7f05ccf1..00000000 --- a/frontend/src/components/FirstChannelJitAlert.tsx +++ /dev/null @@ -1,183 +0,0 @@ -import { AlertTriangleIcon, InfoIcon } from "lucide-react"; -import React from "react"; -import { Link } from "react-router"; -import ExternalLink from "src/components/ExternalLink"; -import { FormattedBitcoinAmount } from "src/components/FormattedBitcoinAmount"; -import { Alert, AlertDescription, AlertTitle } from "src/components/ui/alert"; -import { useBalances } from "src/hooks/useBalances"; -import { useChannels } from "src/hooks/useChannels"; -import { useInfo } from "src/hooks/useInfo"; -import { CreateInvoiceRequest, Transaction } from "src/types"; -import { request } from "src/utils/request"; - -const PROBE_TIMEOUT_MS = 5000; - -export default function FirstChannelJitAlert() { - const { data: info } = useInfo(); - const { data: channels } = useChannels(); - const { data: balances } = useBalances(); - - // a JIT channel only opens when the feature is enabled AND an LSPS2 liquidity - // source is actually configured (jitChannelsEnabled alone is just a settings - // toggle and can be true on backends without an LSPS2 source). - const lsps2Source = info?.jitChannelsEnabled - ? info.jitChannelsLiquiditySource - : undefined; - - const minPaymentSizeMsat = info?.jitChannelsMinPaymentSizeMsat; - - const isJitEnabled = !!lsps2Source && !!channels; - // the user's first received payment opens the channel when they have none yet. - const isFirstChannel = isJitEnabled && channels.length === 0; - - // probe whether a JIT channel can actually be obtained by requesting an - // invoice for the minimum payment size. If it (or waiting for the minimum - // payment size) doesn't succeed within the timeout, we surface a fallback - // alert depending on whether the user already has channels. - const [probeState, setProbeState] = React.useState< - "loading" | "ok" | "failed" - >("loading"); - const deadlineRef = React.useRef(null); - // single-flight the non-idempotent probe invoice: hold the in-flight request - // so effect re-entry (e.g. StrictMode remount) reuses the same POST instead - // of creating a duplicate invoice. Reset when the probe window ends. - const probeRequestRef = React.useRef | null>( - null - ); - - React.useEffect(() => { - if (!isJitEnabled) { - deadlineRef.current = null; - probeRequestRef.current = null; - return; - } - - // start the 5s clock once when we enter JIT mode - it keeps ticking while - // we wait for the minimum payment size to become available. - if (deadlineRef.current === null) { - deadlineRef.current = Date.now() + PROBE_TIMEOUT_MS; - } - - let cancelled = false; - const remainingMs = deadlineRef.current - Date.now(); - if (remainingMs <= 0) { - setProbeState("failed"); - return; - } - - const timer = setTimeout(() => { - if (!cancelled) { - setProbeState("failed"); - } - }, remainingMs); - - // wait for the minimum payment size before requesting the probe invoice. - if (minPaymentSizeMsat) { - // reuse an already in-flight probe so a re-run doesn't issue a second POST. - const probeRequest = - probeRequestRef.current ?? - (probeRequestRef.current = request("/api/invoices", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - amountMsat: minPaymentSizeMsat, - description: "", - } as CreateInvoiceRequest), - })); - probeRequest - .then(() => { - if (!cancelled) { - clearTimeout(timer); - setProbeState("ok"); - } - }) - .catch(() => { - if (!cancelled) { - clearTimeout(timer); - setProbeState("failed"); - } - }); - } - - return () => { - cancelled = true; - clearTimeout(timer); - }; - }, [isJitEnabled, minPaymentSizeMsat]); - - if (!isJitEnabled || probeState === "loading") { - return null; - } - - if (probeState === "failed") { - // no channels yet and a JIT channel couldn't be obtained - the user has no - // receiving capacity at all and will fail to receive until a channel opens. - if (isFirstChannel) { - return ( - - - Can't receive payments yet - - You won't be able to receive payments until you{" "} - - open a channel - - . - - - ); - } - - // they already have channels but a JIT channel couldn't be obtained, so they - // can only receive up to their current capacity without opening one. Wait for - // balances so we don't claim a misleading "0" receivable amount. - if (!balances) { - return null; - } - return ( - - - - You can currently receive up to{" "} - - . If you want to receive a larger payment,{" "} - - open a channel - - . - - - ); - } - - // probe succeeded - only the first-channel case needs an informational alert. - if (!isFirstChannel) { - return null; - } - - return ( - - - First payment opens a channel - - A channel fee applies.{" "} - {!!minPaymentSizeMsat && ( - <> - Minimum payment{" "} - .{" "} - - )} - - Learn more - - - - ); -} diff --git a/frontend/src/components/FixedFloatSwapInFlow.tsx b/frontend/src/components/FixedFloatSwapInFlow.tsx index ff4bbd48..04502a83 100644 --- a/frontend/src/components/FixedFloatSwapInFlow.tsx +++ b/frontend/src/components/FixedFloatSwapInFlow.tsx @@ -4,12 +4,12 @@ import { ExternalLinkIcon, HandCoinsIcon, } from "lucide-react"; +import TickSVG from "public/images/illustrations/tick.svg"; import { useEffect, useState } from "react"; import { FixedFloatButton } from "src/components/FixedFloatButton"; import { FormattedBitcoinAmount } from "src/components/FormattedBitcoinAmount"; import FormattedFiatAmount from "src/components/FormattedFiatAmount"; import LottieLoading from "src/components/LottieLoading"; -import LottieSuccess from "src/components/LottieSuccess"; import { Button } from "src/components/ui/button"; import { Card, @@ -77,7 +77,7 @@ export function FixedFloatSwapInFlow({ return ( - Waiting for Payment... + Waiting for Payment @@ -91,7 +91,7 @@ export function FixedFloatSwapInFlow({ />
- + - + Back to Wallet diff --git a/frontend/src/components/LottieLoading.tsx b/frontend/src/components/LottieLoading.tsx index 8de046d8..0d3e6c92 100644 --- a/frontend/src/components/LottieLoading.tsx +++ b/frontend/src/components/LottieLoading.tsx @@ -1,4 +1,5 @@ -import Lottie from "lottie-react"; +import { useMemo } from "react"; +import Lottie from "react-lottie"; import animationDataDark from "src/assets/lotties/loading-dark.json"; import animationDataLight from "src/assets/lotties/loading-light.json"; import { useTheme } from "src/components/ui/theme-provider"; @@ -6,13 +7,15 @@ import { useTheme } from "src/components/ui/theme-provider"; export default function LottieLoading({ size }: { size?: number }) { const { isDarkMode } = useTheme(); - return ( - + const options = useMemo( + () => ({ + loop: true, + autoplay: true, + animationData: isDarkMode ? animationDataDark : animationDataLight, + rendererSettings: { preserveAspectRatio: "xMidYMid slice" }, + }), + [isDarkMode] ); + + return ; } diff --git a/frontend/src/components/LottieSuccess.tsx b/frontend/src/components/LottieSuccess.tsx deleted file mode 100644 index a846f938..00000000 --- a/frontend/src/components/LottieSuccess.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import Lottie from "lottie-react"; -import animationData from "src/assets/lotties/success-check.json"; - -export default function LottieSuccess({ size = 288 }: { size?: number }) { - return ( -
- -
- ); -} diff --git a/frontend/src/components/OnchainTransactionsList.tsx b/frontend/src/components/OnchainTransactionsList.tsx index 0d4dea5d..a81d087b 100644 --- a/frontend/src/components/OnchainTransactionsList.tsx +++ b/frontend/src/components/OnchainTransactionsList.tsx @@ -1,4 +1,4 @@ -import { BitcoinIcon } from "lucide-react"; +import { LinkIcon } from "lucide-react"; import EmptyState from "src/components/EmptyState"; import Loading from "src/components/Loading"; import OnchainTransactionItem from "src/components/OnchainTransactionItem"; @@ -18,9 +18,12 @@ export function OnchainTransactionsList() { return (
); diff --git a/frontend/src/components/PayLightningInvoice.tsx b/frontend/src/components/PayLightningInvoice.tsx index b9227e95..374f4b6a 100644 --- a/frontend/src/components/PayLightningInvoice.tsx +++ b/frontend/src/components/PayLightningInvoice.tsx @@ -3,6 +3,7 @@ import { CopyIcon, ExternalLinkIcon } from "lucide-react"; import React from "react"; import { FixedFloatButton } from "src/components/FixedFloatButton"; import { FormattedBitcoinAmount } from "src/components/FormattedBitcoinAmount"; +import { LightningIcon } from "src/components/icons/Lightning"; import Loading from "src/components/Loading"; import QRCode from "src/components/QRCode"; import { Button } from "src/components/ui/button"; @@ -27,13 +28,16 @@ export function PayLightningInvoice({ invoice }: PayLightningInvoiceProps) { }; return ( -
-
+
+
-

Waiting for Payment...

+

Waiting for lightning payment...

-
- +
+ +
+ +

@@ -46,19 +50,23 @@ export function PayLightningInvoice({ invoice }: PayLightningInvoiceProps) { }).format(fiatAmount)}

-
- + Pay with other Cryptocurrency - Pay with Crypto
diff --git a/frontend/src/components/PaymentFailedAlert.tsx b/frontend/src/components/PaymentFailedAlert.tsx index 16e57377..9ccb4aab 100644 --- a/frontend/src/components/PaymentFailedAlert.tsx +++ b/frontend/src/components/PaymentFailedAlert.tsx @@ -5,7 +5,7 @@ import { Alert, AlertDescription, AlertTitle } from "src/components/ui/alert"; import { ExternalLinkButton } from "src/components/ui/custom/external-link-button"; import { LoadingButton } from "src/components/ui/custom/loading-button"; import { useChannels } from "src/hooks/useChannels"; -import { sendEvent } from "src/utils/sendEvent"; +import { request } from "src/utils/request"; export function PaymentFailedAlert({ invoice, @@ -19,12 +19,25 @@ export function PaymentFailedAlert({ async function sendDetailsToAlby() { setSendingDetailsToAlby(true); - await sendEvent("payment_failed_details", { - invoice, - errorMessage, - channels, - }); - toast("Thanks for improving Alby Hub."); + try { + await request(`/api/event`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + event: "payment_failed_details", + properties: { + invoice, + errorMessage, + channels, + }, + }), + }); + toast("Thanks for improving Alby Hub."); + } catch (error) { + console.error(error); + } setSendingDetailsToAlby(false); } diff --git a/frontend/src/components/QRCode.tsx b/frontend/src/components/QRCode.tsx index eb97b2cd..6bc2af83 100644 --- a/frontend/src/components/QRCode.tsx +++ b/frontend/src/components/QRCode.tsx @@ -1,116 +1,38 @@ -import { type ReactNode, useEffect, useMemo, useRef } from "react"; -import QRCodeStyling, { type Options } from "qr-code-styling"; -import { BitcoinPaymentIcon } from "src/components/icons/BitcoinPayment"; -import { LightningIcon } from "src/components/icons/Lightning"; +import ReactQRCode from "react-qr-code"; import { cn } from "src/lib/utils"; export type Props = { value: string; size?: number; className?: string; - showAvatar?: boolean; - frameType?: "lightning" | "onchain"; - paymentType?: "lightning" | "onchain"; - centerContent?: ReactNode; - // Use Q when an external overlay covers part of the QR code. + // set the level to Q if there are overlays + // Q will improve error correction (so we can add overlays covering up to 25% of the QR) + // at the price of decreased information density (meaning the QR codes "pixels" have to be + // smaller to encode the same information). + // While that isn't that much of a problem for lightning addresses (because they are usually quite short), + // for invoices that contain larger amount of data those QR codes can get "harder" to read. + // (meaning you have to aim your phone very precisely and have to wait longer for the reader + // to recognize the QR code) level?: "Q" | undefined; }; -function QRCode({ - value, - size = 256, - level, - className, - showAvatar = false, - frameType, - paymentType, - centerContent, -}: Props) { - const resolvedFrameType = paymentType ?? frameType; - const hasCenterContent = Boolean(centerContent); - const containerRef = useRef(null); - const options = useMemo( - () => ({ - type: "svg", - width: size, - height: size, - data: value, - image: showAvatar ? "/icon-lightmode.svg" : undefined, - margin: 0, - qrOptions: { - errorCorrectionLevel: - level ?? (showAvatar || paymentType || hasCenterContent ? "Q" : "M"), - }, - imageOptions: { - crossOrigin: "anonymous", - hideBackgroundDots: true, - imageSize: 0.15, - margin: 4, - }, - dotsOptions: { - color: "var(--qr-foreground)", - type: "dots", - roundSize: false, - }, - cornersSquareOptions: { - color: "var(--qr-foreground)", - type: "extra-rounded", - }, - cornersDotOptions: { - color: "var(--qr-foreground)", - type: "dot", - }, - backgroundOptions: { - color: "var(--qr-background)", - }, - }), - [hasCenterContent, level, paymentType, showAvatar, size, value] - ); - - useEffect(() => { - const container = containerRef.current; - if (!container) { - return; - } - - const qrCode = new QRCodeStyling(options); - qrCode.append(container); - - return () => { - container.replaceChildren(); - }; - }, [options]); +function QRCode({ value, size, level, className }: Props) { + // Do not use dark mode: some apps do not handle it well (e.g. Phoenix) + // const { isDarkMode } = useTheme(); + const fgColor = "#242424"; // isDarkMode ? "#FFFFFF" : "#242424"; + const bgColor = "#FFFFFF"; // isDarkMode ? "#242424" : "#FFFFFF"; return ( -
-
-
-
- {(paymentType || centerContent) && ( -
- {centerContent ?? - (paymentType === "lightning" ? ( - - ) : ( - - ))} -
- )} +
+
); } diff --git a/frontend/src/components/RebalanceChannelDialogContent.tsx b/frontend/src/components/RebalanceChannelDialogContent.tsx index 96a44efb..0e9f3da4 100644 --- a/frontend/src/components/RebalanceChannelDialogContent.tsx +++ b/frontend/src/components/RebalanceChannelDialogContent.tsx @@ -134,7 +134,7 @@ export function RebalanceChannelDialogContent({ }} />

- Fee: 0.5% + Fee: 0.3% {!!amountSat && ( <>  ( diff --git a/frontend/src/components/ReceiveToLightning.tsx b/frontend/src/components/ReceiveToLightning.tsx index 6a4b8761..6c0b4f09 100644 --- a/frontend/src/components/ReceiveToLightning.tsx +++ b/frontend/src/components/ReceiveToLightning.tsx @@ -1,22 +1,10 @@ -import { - ChevronRightIcon, - CopyIcon, - LinkIcon, - ReceiptTextIcon, - ZapIcon, -} from "lucide-react"; -import { Link } from "react-router"; -import FirstChannelJitAlert from "src/components/FirstChannelJitAlert"; +import { CopyIcon, LinkIcon, ReceiptTextIcon, ZapIcon } from "lucide-react"; import Loading from "src/components/Loading"; import QRCode from "src/components/QRCode"; -import { - Accordion, - AccordionContent, - AccordionItem, - AccordionTrigger, -} from "src/components/ui/accordion"; import { Button } from "src/components/ui/button"; -import { Card, CardContent } from "src/components/ui/card"; +import { Card, CardContent, CardFooter } from "src/components/ui/card"; +import { LinkButton } from "src/components/ui/custom/link-button"; +import { Separator } from "src/components/ui/separator"; import { useAlbyMe } from "src/hooks/useAlbyMe"; import { useInfo } from "src/hooks/useInfo"; import { copyToClipboard } from "src/lib/clipboard"; @@ -30,87 +18,43 @@ export function ReceiveToLightning() { } return ( -

- - - - -
-

- {me.lightning_address} -

- -
-
-
- - - - - Other ways to receive - - - -
-

Create Invoice

-

- Request a specific amount with a one-time invoice -

-
- - - {info.supportsBolt12 && ( - - -
-

Lightning Offer

-

- Share a reusable payment code that never expires -

-
- - - )} - - -
-

- On-chain or Other Cryptocurrency -

-

- Swap funds from on-chain bitcoin or other cryptocurrencies -

-
- - -
-
-
-
-
-
+ + + +

+ {me.lightning_address} +

+
+ + + + + + Create Invoice + + {info.backendType === "LDK" && ( + + + Lightning Offer + + )} + + + Receive from On-chain / Other Cryptocurrency + + +
); } diff --git a/frontend/src/components/ReceiveToOnchain.tsx b/frontend/src/components/ReceiveToOnchain.tsx index 4b852946..523ab57f 100644 --- a/frontend/src/components/ReceiveToOnchain.tsx +++ b/frontend/src/components/ReceiveToOnchain.tsx @@ -5,13 +5,13 @@ import { HandCoinsIcon, RefreshCwIcon, } from "lucide-react"; +import TickSVG from "public/images/illustrations/tick.svg"; import { useEffect, useRef, useState } from "react"; import { FixedFloatButton } from "src/components/FixedFloatButton"; import { FormattedBitcoinAmount } from "src/components/FormattedBitcoinAmount"; import FormattedFiatAmount from "src/components/FormattedFiatAmount"; import Loading from "src/components/Loading"; import LottieLoading from "src/components/LottieLoading"; -import LottieSuccess from "src/components/LottieSuccess"; import OnchainAddressDisplay from "src/components/OnchainAddressDisplay"; import QRCode from "src/components/QRCode"; import { Button } from "src/components/ui/button"; @@ -120,13 +120,13 @@ export function ReceiveToOnchain() { target="_blank" className="flex justify-center" > - +
- +
)} - + - + View on Mempool @@ -220,7 +221,7 @@ function DepositSuccess({ Transaction Received! - +

@@ -228,13 +229,13 @@ function DepositSuccess({

- + - + View on Mempool - + Back to Wallet diff --git a/frontend/src/components/Scopes.tsx b/frontend/src/components/Scopes.tsx index b325a265..7982b0bf 100644 --- a/frontend/src/components/Scopes.tsx +++ b/frontend/src/components/Scopes.tsx @@ -19,12 +19,7 @@ import { SheetTitle, } from "src/components/ui/sheet"; import { cn } from "src/lib/utils"; -import { - READ_ONLY_SCOPES, - Scope, - WalletCapabilities, - scopeDescriptions, -} from "src/types"; +import { Scope, WalletCapabilities, scopeDescriptions } from "src/types"; const scopeGroups = ["full_access", "read_only", "isolated", "custom"] as const; type ScopeGroup = (typeof scopeGroups)[number]; @@ -70,8 +65,17 @@ const Scopes: React.FC = ({ }, [capabilities.scopes]); const readOnlyScopes: Scope[] = React.useMemo(() => { + const readOnlyScopes: Scope[] = [ + "get_balance", + "get_info", + "make_invoice", + "lookup_invoice", + "list_transactions", + "notifications", + ]; + return capabilities.scopes.filter((scope) => - READ_ONLY_SCOPES.includes(scope) + readOnlyScopes.includes(scope) ); }, [capabilities.scopes]); diff --git a/frontend/src/components/TransactionItem.tsx b/frontend/src/components/TransactionItem.tsx index 8ad3d5fa..dd60caa0 100644 --- a/frontend/src/components/TransactionItem.tsx +++ b/frontend/src/components/TransactionItem.tsx @@ -277,10 +277,10 @@ function TransactionItem({ tx, transactionListKey }: Props) { {updatedAt.format("D MMMM YYYY, HH:mm")} - {tx.state != "failed" && tx.feesPaidMsat > 0 && ( + {tx.state != "failed" && type == "outgoing" && ( - {type == "outgoing" && ( + {tx.feesPaidMsat > 0 && ( <>  ( {((tx.feesPaidMsat / tx.amountMsat) * 100).toFixed(2)}%) diff --git a/frontend/src/components/TransactionsFilterDialog.tsx b/frontend/src/components/TransactionsFilterDialog.tsx deleted file mode 100644 index 4b72ee44..00000000 --- a/frontend/src/components/TransactionsFilterDialog.tsx +++ /dev/null @@ -1,140 +0,0 @@ -import React from "react"; -import { Button } from "src/components/ui/button"; -import { Checkbox } from "src/components/ui/checkbox"; -import { - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, -} from "src/components/ui/dialog"; -import { Input } from "src/components/ui/input"; -import { Label } from "src/components/ui/label"; -import { ToggleGroup, ToggleGroupItem } from "src/components/ui/toggle-group"; -import { - defaultTransactionFilters, - type TransactionFilters, -} from "src/hooks/useTransactions"; - -const TYPE_OPTIONS: { label: string; value: string }[] = [ - { label: "All", value: "all" }, - { label: "Sent", value: "outgoing" }, - { label: "Received", value: "incoming" }, -]; - -type TransactionsFilterDialogProps = { - open: boolean; - onOpenChange: (open: boolean) => void; - filters: TransactionFilters; - onFiltersChange: (filters: TransactionFilters) => void; -}; - -export function TransactionsFilterDialog({ - open, - onOpenChange, - filters, - onFiltersChange, -}: TransactionsFilterDialogProps) { - const [searchTerm, setSearchTerm] = React.useState(""); - const [type, setType] = React.useState("all"); - const [minAmountSat, setMinAmountSat] = React.useState(""); - const [hideFailed, setHideFailed] = React.useState(false); - - React.useEffect(() => { - if (open) { - setSearchTerm(filters.searchTerm ?? ""); - setType(filters.type ?? "all"); - setMinAmountSat(filters.minAmountSat ? String(filters.minAmountSat) : ""); - setHideFailed(!!filters.hideFailed); - } - }, [open, filters]); - - function onSubmit(e: React.FormEvent) { - e.preventDefault(); - const parsedMinAmountSat = Number(minAmountSat); - onFiltersChange({ - searchTerm: searchTerm.trim() || undefined, - type: type === "incoming" || type === "outgoing" ? type : undefined, - minAmountSat: - Number.isSafeInteger(parsedMinAmountSat) && parsedMinAmountSat > 0 - ? parsedMinAmountSat - : undefined, - hideFailed, - }); - onOpenChange(false); - } - - function onReset() { - onFiltersChange({ ...defaultTransactionFilters }); - onOpenChange(false); - } - - return ( - - -
- - Filter Transactions - - Choose which payments appear in your transaction list. - - -
- - setSearchTerm(e.target.value)} - /> -
-
- - value && setType(value)} - > - {TYPE_OPTIONS.map((option) => ( - - {option.label} - - ))} - -
-
- - setMinAmountSat(e.target.value.trim())} - /> -
-
- setHideFailed(checked === true)} - /> - -
- - - - -
-
-
- ); -} diff --git a/frontend/src/components/TransactionsList.tsx b/frontend/src/components/TransactionsList.tsx index bf2d5ff1..3efaa987 100644 --- a/frontend/src/components/TransactionsList.tsx +++ b/frontend/src/components/TransactionsList.tsx @@ -1,63 +1,36 @@ -import { LucideIcon, ZapIcon } from "lucide-react"; +import { DrumIcon } from "lucide-react"; import { useRef, useState } from "react"; import { CustomPagination } from "src/components/CustomPagination"; import EmptyState from "src/components/EmptyState"; import Loading from "src/components/Loading"; import TransactionItem from "src/components/TransactionItem"; import { LIST_TRANSACTIONS_LIMIT } from "src/constants"; -import { - getTransactionsUrl, - hasActiveTransactionFilters, - useTransactions, -} from "src/hooks/useTransactions"; -import useTransactionFiltersStore from "src/state/TransactionFiltersStore"; +import { getTransactionsUrl, useTransactions } from "src/hooks/useTransactions"; type TransactionsListProps = { appId?: number; - emptyIcon?: LucideIcon; - emptyTitle?: string; - emptyDescription?: string; - emptyVariant?: "dashed" | "muted" | "none"; + showReceiveButton?: boolean; }; function TransactionsList({ appId, - emptyIcon = ZapIcon, - emptyTitle = "No lightning payments yet", - emptyDescription = "Your payments will appear here as you start using your wallet.", - emptyVariant, + showReceiveButton = true, }: TransactionsListProps) { const [page, setPage] = useState(1); - const { filters } = useTransactionFiltersStore(); - - // Reset pagination during render when the filters or app change, so no - // request is made for a page that may not exist under the new list. - const [prevListIdentity, setPrevListIdentity] = useState({ appId, filters }); - if ( - prevListIdentity.appId !== appId || - prevListIdentity.filters !== filters - ) { - setPrevListIdentity({ appId, filters }); - setPage(1); - } - const transactionListRef = useRef(null); const transactionListKey = getTransactionsUrl( appId, LIST_TRANSACTIONS_LIMIT, - page, - filters + page ); const { data: transactionData, isLoading } = useTransactions( appId, false, LIST_TRANSACTIONS_LIMIT, - page, - filters + page ); const transactions = transactionData?.transactions || []; const totalCount = transactionData?.totalCount || 0; - const hasActiveFilters = hasActiveTransactionFilters(filters); const handlePageChange = (page: number) => { setPage(page); @@ -75,14 +48,13 @@ function TransactionsList({
{!transactions.length ? ( ) : ( <> diff --git a/frontend/src/components/TransactionsListMenu.tsx b/frontend/src/components/TransactionsListMenu.tsx index 7aa4cb4b..190599b4 100644 --- a/frontend/src/components/TransactionsListMenu.tsx +++ b/frontend/src/components/TransactionsListMenu.tsx @@ -1,46 +1,27 @@ -import { DownloadIcon, EllipsisVerticalIcon, FunnelIcon } from "lucide-react"; -import { useState } from "react"; -import { TransactionsFilterDialog } from "src/components/TransactionsFilterDialog"; +import { DownloadIcon, EllipsisVerticalIcon } from "lucide-react"; import { Button } from "src/components/ui/button"; import { DropdownMenu, DropdownMenuContent, - DropdownMenuItem, DropdownMenuTrigger, } from "src/components/ui/dropdown-menu"; import { ProDropdownMenuItem } from "src/components/UpgradeDialog"; -import useTransactionFiltersStore from "src/state/TransactionFiltersStore"; import { handleExportTransactions } from "./transactions-utils"; export const TransactionsListMenu = ({ appId }: { appId?: number }) => { - const [filterDialogOpen, setFilterDialogOpen] = useState(false); - const { filters, setFilters } = useTransactionFiltersStore(); - return ( - <> - - - - setFilterDialogOpen(true)}> - - Filter Transactions - - handleExportTransactions(appId)}> - - Export Transactions - - - - - + + + + handleExportTransactions(appId)}> + + Export Transactions + + + ); }; diff --git a/frontend/src/components/WalletActionsMenu.tsx b/frontend/src/components/WalletActionsMenu.tsx index 54b997e8..a7e264e3 100644 --- a/frontend/src/components/WalletActionsMenu.tsx +++ b/frontend/src/components/WalletActionsMenu.tsx @@ -4,12 +4,9 @@ import { CreditCardIcon, DownloadIcon, EllipsisVerticalIcon, - FunnelIcon, } from "lucide-react"; -import { useState } from "react"; import { Link } from "react-router"; import ExternalLink from "src/components/ExternalLink"; -import { TransactionsFilterDialog } from "src/components/TransactionsFilterDialog"; import { Button } from "src/components/ui/button"; import { DropdownMenu, @@ -19,84 +16,55 @@ import { DropdownMenuTrigger, } from "src/components/ui/dropdown-menu"; import { ProDropdownMenuItem } from "src/components/UpgradeDialog"; -import useTransactionFiltersStore from "src/state/TransactionFiltersStore"; import { handleExportTransactions } from "./transactions-utils"; export function WalletActionsMenu({ hasChannelManagement, - isOnchain, }: { hasChannelManagement: boolean; - isOnchain: boolean; }) { - const [filterDialogOpen, setFilterDialogOpen] = useState(false); - const { filters, setFilters } = useTransactionFiltersStore(); - return ( - <> - - - -
- {hasChannelManagement && ( - - - - Swap - - - )} + + + +
+ {hasChannelManagement && ( - - - Recurring + + + Swap - - - - Buy - - - {!isOnchain && } -
- {!isOnchain && ( - <> - setFilterDialogOpen(true)}> - - Filter Transactions - - handleExportTransactions()}> - - Export Transactions - - )} -
-
- {!isOnchain && ( - - )} - + + + + Recurring + + + + + + Buy + + + +
+ handleExportTransactions()}> + + Export Transactions + +
+
); } diff --git a/frontend/src/components/channels/ChannelWaitingForConfirmations.tsx b/frontend/src/components/channels/ChannelWaitingForConfirmations.tsx index bacb4279..1774b0dc 100644 --- a/frontend/src/components/channels/ChannelWaitingForConfirmations.tsx +++ b/frontend/src/components/channels/ChannelWaitingForConfirmations.tsx @@ -49,7 +49,6 @@ export function ChannelWaitingForConfirmations({ icon={FootprintsIcon} title="Browse While You Wait" description="Feel free to leave this page or browse around Alby Hub! We'll send you an email as soon as your channel is active." - variant="dashed" buttonText="Explore Apps" buttonLink="/apps?tab=app-store" /> diff --git a/frontend/src/components/connections/AppStoreDetailHeader.tsx b/frontend/src/components/connections/AppStoreDetailHeader.tsx index 02f4a688..b40c5bd2 100644 --- a/frontend/src/components/connections/AppStoreDetailHeader.tsx +++ b/frontend/src/components/connections/AppStoreDetailHeader.tsx @@ -24,27 +24,36 @@ export function AppStoreDetailHeader({ <> - } title={ -
- {appStoreApp.title} - {!!connectedApps.length && ( - - {" "} - {connectedApps.length > 1 - ? `${connectedApps.length} Connections` - : "Connected"} - - )} -
+ <> +
+ +
+
+ {appStoreApp.title} + {!!connectedApps.length && ( + + {" "} + {connectedApps.length > 1 + ? `${connectedApps.length} Connections` + : "Connected"} + + )} +
+
+ {appStoreApp.description} +
+
+
+ } - description={appStoreApp.description} + description="" contentRight={ contentRight !== undefined ? ( contentRight diff --git a/frontend/src/components/connections/AppTransactionList.tsx b/frontend/src/components/connections/AppTransactionList.tsx index bd9380a1..53c6f435 100644 --- a/frontend/src/components/connections/AppTransactionList.tsx +++ b/frontend/src/components/connections/AppTransactionList.tsx @@ -1,4 +1,3 @@ -import { ArrowDownUpIcon } from "lucide-react"; import TransactionsList from "src/components/TransactionsList"; import { TransactionsListMenu } from "src/components/TransactionsListMenu"; import { @@ -16,13 +15,7 @@ export function AppTransactionList({ appId }: { appId: number }) { - + ); diff --git a/frontend/src/components/connections/BitcoinCardTopupInstallGuide.tsx b/frontend/src/components/connections/BitcoinCardTopupInstallGuide.tsx deleted file mode 100644 index 34e65d3f..00000000 --- a/frontend/src/components/connections/BitcoinCardTopupInstallGuide.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import { useLocation } from "react-router"; -import ExternalLink from "src/components/ExternalLink"; - -// The card topup app supports configuration presets selected via a `provider` -// query param (e.g. linked from the Cards page). Read it straight from the URL -// so the link opens card.albylabs.com pre-configured for the chosen provider. -export function BitcoinCardTopupInstallGuide() { - const provider = new URLSearchParams(useLocation().search).get("provider"); - const url = provider - ? `https://card.albylabs.com?provider=${encodeURIComponent(provider)}` - : "https://card.albylabs.com"; - - return ( -
-
    -
  • - Open{" "} - - card.albylabs.com - {" "} - on the device you'll top up from. -
  • -
  • - - Add it to your home screen - {" "} - (or bookmark it) so you can reopen it later. -
  • -
  • Enter your card's deposit details to set it up.
  • -
-
- ); -} diff --git a/frontend/src/components/connections/ConnectedApps.tsx b/frontend/src/components/connections/ConnectedApps.tsx index 8e2f0e55..a67164c2 100644 --- a/frontend/src/components/connections/ConnectedApps.tsx +++ b/frontend/src/components/connections/ConnectedApps.tsx @@ -88,7 +88,6 @@ function ConnectedApps() { icon={CableIcon} title="Connect Your First App" description="Connect your app of choice, fine-tune permissions and enjoy a seamless and secure wallet experience." - variant="dashed" buttonText="See Recommended Apps" buttonLink="/apps?tab=app-store" /> diff --git a/frontend/src/components/connections/SuggestedAppData.tsx b/frontend/src/components/connections/SuggestedAppData.tsx index 3647a71e..36348acc 100644 --- a/frontend/src/components/connections/SuggestedAppData.tsx +++ b/frontend/src/components/connections/SuggestedAppData.tsx @@ -6,7 +6,6 @@ import albyGo from "src/assets/suggested-apps/alby-go.png"; import albySandbox from "src/assets/suggested-apps/alby-sandbox.png"; import albyCli from "src/assets/suggested-apps/alby.png"; import amethyst from "src/assets/suggested-apps/amethyst.png"; -import bitcoinCardTopup from "src/assets/suggested-apps/bitcoin-card-topup.png"; import bitrefill from "src/assets/suggested-apps/bitrefill.png"; import bitrequest from "src/assets/suggested-apps/bitrequest.png"; import bringin from "src/assets/suggested-apps/bringin.png"; @@ -20,6 +19,7 @@ import fountain from "src/assets/suggested-apps/fountain.png"; import hablanews from "src/assets/suggested-apps/habla-news.png"; import iris from "src/assets/suggested-apps/iris.png"; import jumble from "src/assets/suggested-apps/jumble.png"; +import lendaswap from "src/assets/suggested-apps/lendaswap.png"; import lightningMessageboard from "src/assets/suggested-apps/lightning-messageboard.png"; import lnbits from "src/assets/suggested-apps/lnbits.png"; import lnvps from "src/assets/suggested-apps/lnvps.png"; @@ -30,13 +30,14 @@ import nostrcheckserver from "src/assets/suggested-apps/nostrcheck-server.png"; import nostrudel from "src/assets/suggested-apps/nostrudel.png"; import nostter from "src/assets/suggested-apps/nostter.png"; import nostur from "src/assets/suggested-apps/nostur.png"; +import paperScissorsHodl from "src/assets/suggested-apps/paper-scissors-hodl.png"; import payperq from "src/assets/suggested-apps/payperq.png"; import primal from "src/assets/suggested-apps/primal.png"; import pullthatupjamie from "src/assets/suggested-apps/pullthatupjamie.png"; import runstr from "src/assets/suggested-apps/runstr.png"; -import satsorter from "src/assets/suggested-apps/sat-sorter.png"; -import satoraLogo from "src/assets/suggested-apps/satora.png"; import sats4ai from "src/assets/suggested-apps/sats4ai.png"; +import satsorter from "src/assets/suggested-apps/sat-sorter.png"; +import satoshisauctionhouse from "src/assets/suggested-apps/satoshis-auction-house.png"; import simpleboost from "src/assets/suggested-apps/simple-boost.png"; import snort from "src/assets/suggested-apps/snort.png"; import stackernews from "src/assets/suggested-apps/stacker-news.png"; @@ -53,14 +54,12 @@ import zapplepay from "src/assets/suggested-apps/zapple-pay.png"; import zappybird from "src/assets/suggested-apps/zappy-bird.png"; import zapstore from "src/assets/suggested-apps/zapstore.png"; import zeus from "src/assets/suggested-apps/zeus.png"; -import { BitcoinCardTopupInstallGuide } from "src/components/connections/BitcoinCardTopupInstallGuide"; import ExternalLink from "src/components/ExternalLink"; import { App } from "src/types"; export type AppStoreApp = { id: string; title: string; - legacyTitles?: string[]; description: string; extendedDescription: string; @@ -84,9 +83,6 @@ export type AppStoreApp = { hideConnectionQr?: boolean; internal?: boolean; superuser?: boolean; - // Receive-only apps (e.g. merchant payment receivers) default to read-only - // permissions in the new connection flow. - readonly?: boolean; addedDate?: string; }; @@ -223,33 +219,6 @@ export const appStoreApps: AppStoreApp[] = ( categories: ["audio"], addedDate: "2026-03-12", }, - { - id: "bitcoin-card-topup", - title: "Bitcoin Card Topup", - description: "Top up any crypto debit card instantly with bitcoin", - logo: bitcoinCardTopup, - categories: ["payment-tools"], - extendedDescription: - "A generic top-up app that swaps Lightning sats to a stablecoin and sends them to your card's deposit address. Works with RedotPay, Freedomia, Nexo, Bybit, and any other card that accepts on-chain crypto deposits.", - webLink: "https://card.albylabs.com", - installGuide: , - finalizeGuide: ( - <> -
-
    -
  • Copy the connection secret below.
  • -
  • - In the topup app, tap{" "} - - Connect Wallet - {" "} - and paste the connection secret. -
  • -
-
- - ), - }, { id: "2fiat", title: "2fiat Top up", @@ -799,7 +768,6 @@ export const appStoreApps: AppStoreApp[] = ( }, { id: "sat-sorter", - readonly: true, title: "Sat Sorter", description: "A Bitcoin Budgeting App", webLink: "https://satsorter.com", @@ -899,7 +867,6 @@ export const appStoreApps: AppStoreApp[] = ( }, { id: "bitrequest", - readonly: true, title: "Bitrequest", description: "Non-custodial payment requests", webLink: "https://www.bitrequest.io", @@ -922,6 +889,17 @@ export const appStoreApps: AppStoreApp[] = ( {" "} in your browser, or download the app on iOS or Android

+

+ In the next step, set wallet permissions to{" "} + Custom and + enable: +

+
    +
  • Read your node info
  • +
  • Create invoices
  • +
  • Lookup status of invoices
  • +
  • Read transaction history
  • +
), @@ -961,7 +939,6 @@ export const appStoreApps: AppStoreApp[] = ( }, { id: "btcpay", - readonly: true, title: "BTCPay Server", description: "Bitcoin payment processor", webLink: "https://btcpayserver.org/", @@ -1268,7 +1245,8 @@ export const appStoreApps: AppStoreApp[] = ( title: "wavecard® by wave.space", description: "Spend Bitcoin from your AlbyHub at 150M+ merchants worldwide", - webLink: "https://app.wave.space/?utm_source=albyhub&affiliate=AlbyHub", + webLink: + "https://app.wave.space/spend/?utm_source=albyhub&affiliate=AlbyHub", logo: wavespace, extendedDescription: "The world's first Bitcoin VISA Debit Card that allows you to spend BTC globally, anywhere VISA is accepted – straight from the safety of your own NWC-enabled wallet. ✨ EXCLUSIVE ALBYHUB SPECIAL🐝 → Get 21% cashback on your wavecard transactions (up to 10,000 sats) using code »AlbyHub«", @@ -1280,10 +1258,10 @@ export const appStoreApps: AppStoreApp[] = (
  • Open{" "} - wave.space + wave.space/spend {" "} in your browser and{" "} @@ -1449,7 +1427,6 @@ export const appStoreApps: AppStoreApp[] = ( }, { id: "clams", - readonly: true, title: "Clams", description: "Multi wallet accounting tool", webLink: "https://clams.tech/", @@ -1486,7 +1463,6 @@ export const appStoreApps: AppStoreApp[] = ( }, { id: "nostrcheck-server", - readonly: true, title: "Nostrcheck Server", description: "Sovereign Nostr services", webLink: "https://github.com/quentintaranpino/nostrcheck-server", @@ -1794,7 +1770,6 @@ export const appStoreApps: AppStoreApp[] = ( }, { id: "nakapay", - readonly: true, title: "NakaPay", description: "Non-custodial Lightning payments for businesses via NWC", webLink: "https://www.nakapay.app", @@ -2067,6 +2042,47 @@ export const appStoreApps: AppStoreApp[] = ( ), categories: ["social-media"], }, + { + id: "paper-scissors-hodl", + title: "Paper Scissors HODL", + description: "Paper Scissors Rock with bitcoin at stake", + webLink: "https://paper-scissors-hodl.fly.dev", + logo: paperScissorsHodl, + extendedDescription: + "Uses your Hub to pay to play a round, and receive the reward if you win", + installGuide: ( + <> +

    + Open{" "} + + Paper Scissors HODL + {" "} + in your browser +

    + + ), + finalizeGuide: ( + <> +
    +

    In Paper Scissors HODL

    +
      +
    • Start playing until the Bitcoin Connect screen pops up
    • +
    • + Choose{" "} + + Nostr Wallet Connect + +
    • +
    • Paste the connection secret from Alby Hub
    • +
    +
    + + ), + categories: ["games"], + }, { id: "pullthatupjamie-ai", title: "Pull That Up Jamie!", @@ -2204,40 +2220,10 @@ export const appStoreApps: AppStoreApp[] = ( title: "Bitrefill", description: "Live on bitcoin", extendedDescription: "Buy gift cards and e-sims with no KYC", + internal: true, webLink: "https://bitrefill.com", logo: bitrefill, categories: ["shopping"], - appleLink: - "https://apps.apple.com/us/app/bitrefill-esims-gift-cards/id1378102623", - playLink: - "https://play.google.com/store/apps/details?id=com.bitrefill.app&hl=en", - installGuide: ( - <> -

    - Open{" "} - - Bitrefill - {" "} - in your browser, or download the app on iOS or Android -

    - - ), - finalizeGuide: ( - <> -
    -

    In Bitrefill

    -
      -
    • - Go to settings {"->"} wallets {"->"} lightning {"->"} payments. -
    • -
    • Paste the connection secret to connect Alby Hub
    • -
    -
    - - ), }, { id: "bringin", @@ -2333,9 +2319,49 @@ export const appStoreApps: AppStoreApp[] = ( ), categories: ["social-media"], }, + { + id: "satoshis-auction-house", + title: "Satoshi's Auction House", + description: "Bitcoin-powered auction platform", + webLink: "https://satoshisauction.house", + logo: satoshisauctionhouse, + extendedDescription: + "Buy and sell items through Bitcoin-powered auctions directly from your Hub", + installGuide: ( + <> +

    + Open{" "} + + Satoshi's Auction House + {" "} + in your browser +

    + + ), + finalizeGuide: ( + <> +
    +

    In Satoshi's Auction House

    +
      +
    • + Click on the Hamburger menu on the top right and click{" "} + Settings +
    • +
    • + Paste the connection secret from Alby Hub into the receive-only + connection secret field +
    • +
    +
    + + ), + categories: ["shopping"], + }, { id: "takemysats", - readonly: true, title: "Take My Sats", description: "Create your online store and accept Bitcoin payments", webLink: "https://www.takemysats.com", @@ -2441,24 +2467,22 @@ export const appStoreApps: AppStoreApp[] = ( categories: ["misc"], }, { - // Keep the legacy app store ID so existing LendaSwap connections still match this entry. id: "lendaswap", - title: "Satora", - legacyTitles: ["LendaSwap"], + title: "LendaSwap", description: "Self-custodial Bitcoin ↔ Stablecoin atomic swaps", - webLink: "https://app.satora.io/?ref=lnds_e3f8dd88_f7db93dbf176", - logo: satoraLogo, + webLink: "https://lendaswap.com/?ref=lnds_e3f8dd88_f7db93dbf176", + logo: lendaswap, extendedDescription: - "Swap between Lightning Bitcoin and stablecoins (USDC, USDT) on Polygon, Arbitrum, and Ethereum. Satora uses your Hub to pay swap invoices and generate receiving invoices — all self-custodial via HTLCs.", + "Swap between Lightning Bitcoin and stablecoins (USDC, USDT) on Polygon, Arbitrum, and Ethereum. LendaSwap uses your Hub to pay swap invoices and generate receiving invoices — all self-custodial via HTLCs.", installGuide: ( <>

    Open{" "} - Satora + LendaSwap {" "} in your browser

    @@ -2467,7 +2491,7 @@ export const appStoreApps: AppStoreApp[] = ( finalizeGuide: ( <>
    -

    In Satora

    +

    In LendaSwap

    • Click the{" "} @@ -2509,7 +2533,6 @@ export const getAppStoreApp = (app: App) => { return appStoreApps.find( (suggestedApp) => suggestedApp.id === (app.metadata?.app_store_app_id ?? "") || - app.name.includes(suggestedApp.title) || - suggestedApp.legacyTitles?.some((title) => app.name.includes(title)) + app.name.includes(suggestedApp.title) ); }; diff --git a/frontend/src/components/connections/SuggestedApps.tsx b/frontend/src/components/connections/SuggestedApps.tsx index a097c081..239c4020 100644 --- a/frontend/src/components/connections/SuggestedApps.tsx +++ b/frontend/src/components/connections/SuggestedApps.tsx @@ -28,12 +28,7 @@ function AppCard(app: AppStoreApp) { />
      {app.title} - - {app.description} - {app.legacyTitles?.length - ? ` (Previously ${app.legacyTitles.join(", ")})` - : ""} - + {app.description}
    diff --git a/frontend/src/components/home/widgets/AlbyAccountWidget.tsx b/frontend/src/components/home/widgets/AlbyAccountWidget.tsx new file mode 100644 index 00000000..8cb0feda --- /dev/null +++ b/frontend/src/components/home/widgets/AlbyAccountWidget.tsx @@ -0,0 +1,51 @@ +import { ExternalLinkIcon } from "lucide-react"; +import { AlbyHead } from "src/components/images/AlbyHead"; +import { + Card, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from "src/components/ui/card"; +import { ExternalLinkButton } from "src/components/ui/custom/external-link-button"; +import { useInfo } from "src/hooks/useInfo"; + +export function AlbyAccountWidget() { + const { data: info } = useInfo(); + + if (!info || !info.albyAccountConnected) { + return null; + } + + return ( + + +
    +
    + +
    +
    + +
    + Alby Account +
    +
    + + Get an Alby Account with a web wallet interface, lightning address + and other features. + +
    +
    +
    + + + Open Alby Account + + + +
    + ); +} diff --git a/frontend/src/components/home/widgets/AlbyExtensionWidget.tsx b/frontend/src/components/home/widgets/AlbyExtensionWidget.tsx new file mode 100644 index 00000000..9256ce6f --- /dev/null +++ b/frontend/src/components/home/widgets/AlbyExtensionWidget.tsx @@ -0,0 +1,50 @@ +import { ExternalLinkIcon } from "lucide-react"; +import { AlbyHead } from "src/components/images/AlbyHead"; +import { + Card, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from "src/components/ui/card"; +import { ExternalLinkButton } from "src/components/ui/custom/external-link-button"; + +export function AlbyExtensionWidget() { + /* eslint-disable @typescript-eslint/no-explicit-any */ + const extensionInstalled = (window as any).alby !== undefined; + + if (extensionInstalled) { + return null; + } + + return ( + + +
    +
    + +
    +
    + +
    + Alby Browser Extension +
    +
    + + Seamless bitcoin payments in your favorite internet browser. + +
    +
    +
    + + + Install Alby Extension + + + +
    + ); +} diff --git a/frontend/src/components/home/widgets/AlbyGoWidget.tsx b/frontend/src/components/home/widgets/AlbyGoWidget.tsx new file mode 100644 index 00000000..9aabc2be --- /dev/null +++ b/frontend/src/components/home/widgets/AlbyGoWidget.tsx @@ -0,0 +1,42 @@ +import albyGo from "src/assets/suggested-apps/alby-go.png"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "src/components/ui/card"; +import { LinkButton } from "src/components/ui/custom/link-button"; + +export function AlbyGoWidget() { + return ( + + +
    +
    + Alby Go +
    +
    + +
    + Alby Go +
    +
    + + The easiest Bitcoin mobile app that works great with Alby Hub. + +
    +
    +
    + + + Open + + +
    + ); +} diff --git a/frontend/src/components/home/widgets/StoriesWidget.tsx b/frontend/src/components/home/widgets/StoriesWidget.tsx deleted file mode 100644 index c1c7b466..00000000 --- a/frontend/src/components/home/widgets/StoriesWidget.tsx +++ /dev/null @@ -1,254 +0,0 @@ -import { XIcon } from "lucide-react"; -import React from "react"; -import { Link } from "react-router"; -import useSWR from "swr"; -import ExternalLink from "src/components/ExternalLink"; -import { Button } from "src/components/ui/button"; -import { - Card, - CardContent, - CardHeader, - CardTitle, -} from "src/components/ui/card"; -import { - Dialog, - DialogClose, - DialogContent, - DialogDescription, - DialogTitle, -} from "src/components/ui/dialog"; -import { localStorageKeys } from "src/constants"; -import { cn } from "src/lib/utils"; -import { swrFetcher } from "src/utils/swr"; - -type StoryCta = { - label: string; - url: string; - openInNewTab: boolean; -}; - -type Story = { - id: string; - title: string; - avatar: string; - videoId?: string; - cta?: StoryCta; -}; - -type StoryApiResponse = { - id: number; - title: string; - avatar: string; - videoId?: string; - cta?: StoryCta; -}; - -function youTubeEmbedUrl(videoId: string) { - return `https://www.youtube-nocookie.com/embed/${videoId}?autoplay=1&rel=0`; -} - -function loadViewedStoryIds(): Set { - try { - const raw = localStorage.getItem(localStorageKeys.homeStoriesViewed); - if (!raw) { - return new Set(); - } - const parsed = JSON.parse(raw) as unknown; - if (!Array.isArray(parsed)) { - return new Set(); - } - return new Set(parsed.filter((id): id is string => typeof id === "string")); - } catch { - return new Set(); - } -} - -function persistViewedStoryIds(ids: Set) { - try { - localStorage.setItem( - localStorageKeys.homeStoriesViewed, - JSON.stringify([...ids]) - ); - } catch { - // ignore quota / private mode - } -} - -function StoryAvatar({ story, viewed }: { story: Story; viewed: boolean }) { - return ( -
    -
    - {`${story.title} -
    -
    - ); -} - -export function StoriesWidget() { - const { data, error, isLoading } = useSWR( - "/api/alby/stories", - swrFetcher - ); - const [activeStory, setActiveStory] = React.useState(null); - const [viewedIds, setViewedIds] = - React.useState>(loadViewedStoryIds); - - const stories = React.useMemo( - () => - error || !data - ? [] - : data.map((story) => ({ - id: String(story.id), - title: story.title, - avatar: story.avatar, - videoId: story.videoId, - cta: story.cta, - })), - [data, error] - ); - - const markStoryViewed = React.useCallback((storyId: string) => { - setViewedIds((prev) => { - if (prev.has(storyId)) { - return prev; - } - const next = new Set(prev); - next.add(storyId); - persistViewedStoryIds(next); - return next; - }); - }, []); - - if (!isLoading && stories.length === 0) { - return null; - } - - return ( - <> - - - Stories - - -
    - {isLoading && ( - - Loading stories... - - )} - {!isLoading && - stories.map((story) => { - const viewed = viewedIds.has(story.id); - return ( - - ); - })} -
    -
    -
    - - !open && setActiveStory(null)} - > - - {activeStory && ( -
    - {activeStory.title} - - Watch the latest update - - - {activeStory.videoId && ( -
    - +
    + + + + Confirm payment + +
    +
    +
    Description
    +
    {invoice?.description}
    +
    +
    +
    Amount
    +
    + + + + +
    +
    +
    + + Cancel + + {loading && } + Pay now + + +
    +
    + + ); +} diff --git a/frontend/src/screens/internal-apps/BuzzPay.tsx b/frontend/src/screens/internal-apps/BuzzPay.tsx index 8168fe04..70591100 100644 --- a/frontend/src/screens/internal-apps/BuzzPay.tsx +++ b/frontend/src/screens/internal-apps/BuzzPay.tsx @@ -1,6 +1,7 @@ import { AlertTriangleIcon, CopyIcon, ExternalLinkIcon } from "lucide-react"; import React from "react"; import { toast } from "sonner"; +import buzzpay from "src/assets/suggested-apps/buzzpay.png"; import { AppDetailConnectedApps } from "src/components/connections/AppDetailConnectedApps"; import { AppStoreDetailHeader } from "src/components/connections/AppStoreDetailHeader"; import { appStoreApps } from "src/components/connections/SuggestedAppData"; @@ -81,24 +82,20 @@ export function BuzzPay() { -
    +
    +
    -
    +
    - - diff --git a/frontend/src/screens/onchain/DepositBitcoin.tsx b/frontend/src/screens/onchain/DepositBitcoin.tsx index dfc836ce..75b95405 100644 --- a/frontend/src/screens/onchain/DepositBitcoin.tsx +++ b/frontend/src/screens/onchain/DepositBitcoin.tsx @@ -1,4 +1,5 @@ import { + CircleCheckIcon, CopyIcon, CreditCardIcon, ExternalLinkIcon, @@ -11,7 +12,6 @@ import { FormattedBitcoinAmount } from "src/components/FormattedBitcoinAmount"; import FormattedFiatAmount from "src/components/FormattedFiatAmount"; import Loading from "src/components/Loading"; import LottieLoading from "src/components/LottieLoading"; -import LottieSuccess from "src/components/LottieSuccess"; import { MempoolAlert } from "src/components/MempoolAlert"; import OnchainAddressDisplay from "src/components/OnchainAddressDisplay"; import QRCode from "src/components/QRCode"; @@ -129,7 +129,7 @@ export default function DepositBitcoin() { target="_blank" className="flex justify-center" > - +
    @@ -137,11 +137,11 @@ export default function DepositBitcoin() {
    -
    +
    {!loadingAddress && } @@ -149,7 +149,7 @@ export default function DepositBitcoin() {
    @@ -201,14 +201,14 @@ function DepositPending({
    )} -
    +
    View on Mempool - +
    @@ -232,21 +232,21 @@ function DepositSuccess({ Payment Received! - +

    -
    +
    View on Mempool - +
    diff --git a/frontend/src/screens/peers/ConnectPeer.tsx b/frontend/src/screens/peers/ConnectPeer.tsx index 601832ea..b6c1583c 100644 --- a/frontend/src/screens/peers/ConnectPeer.tsx +++ b/frontend/src/screens/peers/ConnectPeer.tsx @@ -9,7 +9,6 @@ import { Label } from "src/components/ui/label"; import { splitSocketAddress } from "src/lib/utils"; import { ConnectPeerRequest } from "src/types"; import { request } from "src/utils/request"; -import { safeReturnToUrl } from "src/utils/safeReturnToUrl"; export default function ConnectPeer() { const navigate = useNavigate(); @@ -19,7 +18,7 @@ export default function ConnectPeer() { const [connectionString, setConnectionString] = React.useState( queryParams.get("peer") ?? "" ); - const returnTo = safeReturnToUrl(queryParams.get("return_to")) ?? ""; + const returnTo = queryParams.get("return_to") ?? ""; const handleSubmit = async (event: React.FormEvent) => { event.preventDefault(); diff --git a/frontend/src/screens/settings/About.tsx b/frontend/src/screens/settings/About.tsx index b2e254ba..161958c4 100644 --- a/frontend/src/screens/settings/About.tsx +++ b/frontend/src/screens/settings/About.tsx @@ -1,31 +1,13 @@ -import { ExternalLinkIcon } from "lucide-react"; -import ExternalLink from "src/components/ExternalLink"; import Loading from "src/components/Loading"; import SettingsHeader from "src/components/SettingsHeader"; import { Badge } from "src/components/ui/badge"; import { useAlbyMe } from "src/hooks/useAlbyMe"; -import { useNodeDetails } from "src/hooks/useNodeDetails"; -import { backendTypeConfigs } from "src/lib/backendType"; import { useInfo } from "src/hooks/useInfo"; export function About() { const { data: info } = useInfo(); const { data: albyMe, error: albyMeError } = useAlbyMe(); - const lsps2Source = info?.jitChannelsLiquiditySource; - const lsps2Pubkey = lsps2Source?.includes("@") - ? lsps2Source.split("@")[0] - : undefined; - const { data: lsps2NodeDetails } = useNodeDetails(lsps2Pubkey); - const lsps2Label = - lsps2NodeDetails?.alias || - (lsps2Pubkey ? lsps2Pubkey.slice(0, 8) + "..." : lsps2Source); - const lsps2MinPaymentSizeSat = info?.jitChannelsMinPaymentSizeMsat - ? Math.ceil(info.jitChannelsMinPaymentSizeMsat / 1000) - : undefined; - const lsps2MaxPaymentSizeSat = info?.jitChannelsMaxPaymentSizeMsat - ? Math.floor(info.jitChannelsMaxPaymentSizeMsat / 1000) - : undefined; if (!info || (info.albyAccountConnected && !albyMe && !albyMeError)) { return ; @@ -59,33 +41,10 @@ export function About() {

    Lightning Node Backend

    -
    -
    - {backendTypeConfigs[info.backendType].icon} -
    -

    {backendTypeConfigs[info.backendType].title}

    -
    +

    + {info.backendType} +

    - {info.databaseType && ( -
    -

    Database Storage

    -

    - {info.databaseType === "postgres" - ? "PostgreSQL" - : info.databaseType === "sqlite" - ? "SQLite" - : info.databaseType} -

    -
    - )} - {info.backendType === "LDK" && ( -
    -

    VSS

    -

    - {info.ldkVssEnabled ? `Enabled (${info.ldkVssUrl})` : "Disabled"} -

    -
    - )} {info.chainDataSourceType && (

    Chain Data Source

    @@ -97,40 +56,6 @@ export function About() {
    )} - {info.jitChannelsLiquiditySource && ( -
    -

    - Just-in-Time channels Liquidity Source LSPS2 -

    -
    - {lsps2Pubkey ? ( - - {lsps2Label} - - - ) : ( -

    {lsps2Label}

    - )} -

    {info.jitChannelsLiquiditySource}

    - {(lsps2MinPaymentSizeSat || lsps2MaxPaymentSizeSat) && ( -

    - JIT payment size:{" "} - {lsps2MinPaymentSizeSat - ? new Intl.NumberFormat().format(lsps2MinPaymentSizeSat) - : "?"} - {" - "} - {lsps2MaxPaymentSizeSat - ? new Intl.NumberFormat().format(lsps2MaxPaymentSizeSat) - : "?"}{" "} - sats -

    - )} -
    -
    - )}

    Nostr Relays

    {info.relays.map((relay) => ( diff --git a/frontend/src/screens/settings/Backup.tsx b/frontend/src/screens/settings/Backup.tsx index d332cdb9..483da09a 100644 --- a/frontend/src/screens/settings/Backup.tsx +++ b/frontend/src/screens/settings/Backup.tsx @@ -41,12 +41,7 @@ import { InfoResponse, MnemonicResponse } from "src/types"; import { request } from "src/utils/request"; export default function Backup() { - const { - data: info, - hasMnemonic, - hasChannelManagement, - hasNodeBackup, - } = useInfo(); + const { data: info, hasMnemonic } = useInfo(); const { data: me } = useAlbyMe(); const [unlockPassword, setUnlockPassword] = useState(""); const [decryptedMnemonic, setDecryptedMnemonic] = useState(""); @@ -85,11 +80,9 @@ export default function Backup() { description={ <> - Backup your recovery phrase - {hasChannelManagement && " and channel states"}. These backups are - for disaster recovery only. - {hasNodeBackup && - " To migrate your node, please use the migration tool."}{" "} + Backup your recovery phrase and channel states. These backups are + for disaster recovery only. To migrate your node, please use the + migration tool.{" "} Recovery Phrase

    Your recovery phrase is a group of 12 random words that back - up your wallet{" "} - {info?.backendType === "LDK" ? "on-chain balance" : "balance"} - . Using them is the only way to recover access to your wallet - on another machine or when you lose your unlock password. + up your wallet on-chain balance. Using them is the only way to + recover access to your wallet on another machine or when you + lose your unlock password.

    @@ -174,117 +166,113 @@ export default function Backup() { )} - {hasChannelManagement && ( -
    -
    -

    Channels Backup

    -

    - Your lightning balance can only be recovered on-chain by closing - your lightning channels. In case of recovery of your Alby Hub a - request will be sent to your peers to close your existing - channels. To recover the funds from these channels, a channel - backup needs to be created every time you open a new channel. -

    -
    +
    +
    +

    Channels Backup

    +

    + Your lightning balance can only be recovered on-chain by closing + your lightning channels. In case of recovery of your Alby Hub a + request will be sent to your peers to close your existing + channels. To recover the funds from these channels, a channel + backup needs to be created every time you open a new channel. +

    +
    -
    -
    -

    - Automatic Channels Backup -

    - {info?.albyAccountConnected ? ( - Active - ) : ( - Recommended - )} -
    +
    +
    +

    Automatic Channels Backup

    {info?.albyAccountConnected ? ( - <> -

    - Your channel state is backed up automatically after each - channel creation. Using an external recovery tool and your - recovery phrase, you can recover your funds from channels to - your on-chain balance as long as your channel partners are - online. -

    - {info?.vssSupported && ( - <> -
    -

    - Dynamic Channels Backup With Instant Recovery -

    - {me?.subscription.plan_code && info.ldkVssEnabled ? ( - Active - ) : ( - Alby Cloud - )} -
    -

    - When enabled, your channels state is dynamically updated - and stored end-to-end encrypted by Alby's Versioned - Storage Service. This allows you to recover your - lightning balance with your recovery phrase alone, - without having to close your channels. -

    - - {!info.ldkVssEnabled && - (!me?.subscription.plan_code ? ( - - - - ) : ( - - ))} - - )} - + Active ) : ( -
    -
    -

    - Link your Alby Account to enable automatic channel backups - after each channel creation. -

    - -
    -
    -
    -

    - Manual Channels Backup -

    - Active -
    -

    - - To backup your channels state manually, without Alby - Account linked, follow the - {" "} - - manual backups guide - - -

    -
    -
    + Recommended )}
    -
    - )} + {info?.albyAccountConnected ? ( + <> +

    + Your channel state is backed up automatically after each + channel creation. Using an external recovery tool and your + recovery phrase, you can recover your funds from channels to + your on-chain balance as long as your channel partners are + online. +

    + {info?.vssSupported && ( + <> +
    +

    + Dynamic Channels Backup With Instant Recovery +

    + {me?.subscription.plan_code && info.ldkVssEnabled ? ( + Active + ) : ( + Alby Cloud + )} +
    +

    + When enabled, your channels state is dynamically updated + and stored end-to-end encrypted by Alby's Versioned + Storage Service. This allows you to recover your lightning + balance with your recovery phrase alone, without having to + close your channels. +

    - {!hasMnemonic && !hasChannelManagement && !info?.vssSupported && ( + {!info.ldkVssEnabled && + (!me?.subscription.plan_code ? ( + + + + ) : ( + + ))} + + )} + + ) : ( +
    +
    +

    + Link your Alby Account to enable automatic channel backups + after each channel creation. +

    + +
    +
    +
    +

    + Manual Channels Backup +

    + Active +
    +

    + + To backup your channels state manually, without Alby + Account linked, follow the + {" "} + + manual backups guide + + +

    +
    +
    + )} +
    +
    + + {!hasMnemonic && !info?.vssSupported && (

    No recovery phrase or channel state backup present.

    diff --git a/frontend/src/screens/settings/DeveloperSettings.tsx b/frontend/src/screens/settings/DeveloperSettings.tsx index 2dfe1b98..68fe28c8 100644 --- a/frontend/src/screens/settings/DeveloperSettings.tsx +++ b/frontend/src/screens/settings/DeveloperSettings.tsx @@ -13,12 +13,10 @@ import { Separator } from "src/components/ui/separator"; import { useAlbyMe } from "src/hooks/useAlbyMe"; import { copyToClipboard } from "src/lib/clipboard"; import { AuthTokenResponse } from "src/types"; -import { isHttpMode } from "src/utils/isHttpMode"; import { request } from "src/utils/request"; export default function DeveloperSettings() { const { data: albyMe } = useAlbyMe(); - const _isHttpMode = isHttpMode(); const [token, setToken] = React.useState(); const [tokenPermission, setTokenPermission] = React.useState(); const [expiryDays, setExpiryDays] = React.useState("365"); @@ -105,14 +103,7 @@ export default function DeveloperSettings() { be removed entirely in the future.
    - {!_isHttpMode && ( -
    - API access is not available in the desktop app because it does not - expose an HTTP API. To use the API, install the Alby Hub server - version instead. -
    - )} - {_isHttpMode && !token && !showCreateTokenForm && ( + {!token && !showCreateTokenForm && (
    +
    +
    + +
    + + Your recovery phrase cannot restore funds from lightning channels. + If you had active channels on a different device, contact Alby + support before proceeding. + +
    +
    + setIsBackedUp(!backedUp)} + /> + +
    ); diff --git a/frontend/src/screens/setup/RestoreNode.tsx b/frontend/src/screens/setup/RestoreNode.tsx index e364eca4..e581ac2e 100644 --- a/frontend/src/screens/setup/RestoreNode.tsx +++ b/frontend/src/screens/setup/RestoreNode.tsx @@ -52,9 +52,9 @@ export function RestoreNode() { />

    - If you're running in a cloud VM or linux service, your Alby Hub will - restart automatically. Otherwise, please manually restart your Alby - Hub to finish the restore process. + If you're running in the cloud, your Alby Hub will restart + automatically. Otherwise, please manually restart your Alby Hub to + finish the restore process.

    Waiting for restart...

    @@ -158,9 +158,9 @@ export function RestoreNode() { down.

    - If you're running in a cloud VM or linux service, your Alby - Hub will restart automatically. Otherwise, please manually - restart your Alby Hub to finish the restore process. + If you're running in the cloud, your Alby Hub will restart + automatically. Otherwise, please manually restart your Alby + Hub to finish the restore process.

    diff --git a/frontend/src/screens/setup/SetupAdvanced.tsx b/frontend/src/screens/setup/SetupAdvanced.tsx index 21bde4d8..5ee6b738 100644 --- a/frontend/src/screens/setup/SetupAdvanced.tsx +++ b/frontend/src/screens/setup/SetupAdvanced.tsx @@ -1,15 +1,8 @@ -import { useEffect } from "react"; import Container from "src/components/Container"; import TwoColumnLayoutHeader from "src/components/TwoColumnLayoutHeader"; import { LinkButton } from "src/components/ui/custom/link-button"; -import useSetupStore from "src/state/SetupStore"; export function SetupAdvanced() { - useEffect(() => { - // in case the user goes back, reset the imported mnemonic flag - useSetupStore.getState().setHasImportedMnemonic(false); - }, []); - return (
    diff --git a/frontend/src/screens/setup/SetupNode.tsx b/frontend/src/screens/setup/SetupNode.tsx index 0cd5349e..4e245b13 100644 --- a/frontend/src/screens/setup/SetupNode.tsx +++ b/frontend/src/screens/setup/SetupNode.tsx @@ -1,20 +1,55 @@ -import React from "react"; +import React, { ReactElement } from "react"; import { useNavigate } from "react-router"; import Container from "src/components/Container"; import TwoColumnLayoutHeader from "src/components/TwoColumnLayoutHeader"; +import { LDKIcon } from "src/components/icons/LDK"; +import { PhoenixdIcon } from "src/components/icons/Phoenixd"; import { Button } from "src/components/ui/button"; import { cn } from "src/lib/utils"; import { BackendType } from "src/types"; +import cashu from "src/assets/images/node/cashu.png"; +import cln from "src/assets/images/node/cln.png"; +import lnd from "src/assets/images/node/lnd.png"; import { backendTypeConfigs } from "src/lib/backendType"; import useSetupStore from "src/state/SetupStore"; -const backendTypeDisplayConfigList = Object.entries(backendTypeConfigs).map( - (entry) => ({ - ...entry[1], - backendType: entry[0] as BackendType, - }) -); +type BackendTypeDisplayConfig = { + title: string; + icon: ReactElement; +}; + +const backendTypeDisplayConfigs: Partial< + Record +> = { + LDK: { + title: "LDK", + icon: , + }, + PHOENIX: { + title: "phoenixd", + icon: , + }, + LND: { + title: "LND", + icon: , + }, + CASHU: { + title: "Cashu Mint", + icon: , + }, + CLN: { + title: "CLN", + icon: , + }, +}; + +const backendTypeDisplayConfigList = Object.entries( + backendTypeDisplayConfigs +).map((entry) => ({ + ...entry[1], + backendType: entry[0] as BackendType, +})); export function SetupNode() { const navigate = useNavigate(); @@ -29,8 +64,7 @@ export function SetupNode() { navigate(`/setup/node/${selectedBackendType.toLowerCase()}`); } - const hasImportedMnemonic = - !!setupStore.nodeInfo.mnemonic && setupStore.hasImportedMnemonic; + const hasImportedMnemonic = !!setupStore.nodeInfo.mnemonic; return ( diff --git a/frontend/src/screens/setup/SetupSecurity.tsx b/frontend/src/screens/setup/SetupSecurity.tsx index 74f2467a..e170ec41 100644 --- a/frontend/src/screens/setup/SetupSecurity.tsx +++ b/frontend/src/screens/setup/SetupSecurity.tsx @@ -1,7 +1,5 @@ import { - ClockIcon, HandCoinsIcon, - InfoIcon, LandmarkIcon, ShieldAlertIcon, UnlockIcon, @@ -14,12 +12,6 @@ import TwoColumnLayoutHeader from "src/components/TwoColumnLayoutHeader"; import { Button } from "src/components/ui/button"; import { Checkbox } from "src/components/ui/checkbox"; import { Label } from "src/components/ui/label"; -import { - Tooltip, - TooltipContent, - TooltipProvider, - TooltipTrigger, -} from "src/components/ui/tooltip"; import { useInfo } from "src/hooks/useInfo"; import useSetupStore from "src/state/SetupStore"; @@ -44,18 +36,17 @@ export function SetupSecurity() { />
    - {store.nodeInfo.backendType !== "CASHU" && - store.nodeInfo.backendType !== "BARK" && ( -
    -
    - -
    - - Alby Hub is a spending wallet - do not keep all your savings - on it! - + {store.nodeInfo.backendType !== "CASHU" && ( +
    +
    +
    - )} + + Alby Hub is a spending wallet - do not keep all your savings on + it! + +
    + )} {store.nodeInfo.backendType === "CASHU" && (
    @@ -67,64 +58,6 @@ export function SetupSecurity() {
    )} - {store.nodeInfo.backendType === "BARK" && ( - <> -
    - - - Bark is in beta - use at your own risk! - -
    -
    - - - Your funds must be refreshed periodically{" "} - - - - - - - Your virtual UTXOs (VTXOs) will be refreshed - automatically to ensure you maintain ownership. - Refreshes may incur a small fee. Keep your Alby Hub - online so your funds do not expire. - - - - -
    -
    - - - Unilateral exit is not implemented in Alby Hub yet{" "} - - - - - - - You will need to take your hub wallet data and execute - the exit yourself. - - - - -
    - -

    - By using Bark you agree to{" "} - - Second's Terms of Service - - . -

    -
    - - )}
    @@ -148,19 +81,6 @@ export function SetupSecurity() { choose the LDK node type.
    - ) : store.hasImportedMnemonic && - store.nodeInfo.backendType === "LDK" ? ( -
    -
    - -
    - - Your recovery phrase can only restore funds from lightning - channels if you have dynamic channel backups enabled. If you had - active channels on a different device, contact Alby support - before proceeding. - -
    ) : (
    diff --git a/frontend/src/screens/setup/node/BarkForm.tsx b/frontend/src/screens/setup/node/BarkForm.tsx deleted file mode 100644 index 153b3e16..00000000 --- a/frontend/src/screens/setup/node/BarkForm.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import { wordlist } from "@scure/bip39/wordlists/english.js"; -import { useEffect } from "react"; -import { useNavigate, useSearchParams } from "react-router"; -import useSetupStore from "src/state/SetupStore"; - -import * as bip39 from "@scure/bip39"; -import Loading from "src/components/Loading"; - -export function BarkForm() { - const navigate = useNavigate(); - const [searchParams] = useSearchParams(); - - useEffect(() => { - if (!useSetupStore.getState().nodeInfo.mnemonic) { - useSetupStore.getState().updateNodeInfo({ - mnemonic: bip39.generateMnemonic(wordlist, 128), - }); - } - useSetupStore.getState().updateNodeInfo({ - backendType: "BARK", - }); - navigate("/setup/security", { - replace: true, - }); - }, [navigate, searchParams]); - - return ( - <> - Loading... · Alby Hub - - - ); -} diff --git a/frontend/src/screens/setup/node/LDKForm.tsx b/frontend/src/screens/setup/node/LDKForm.tsx index 5ad5f891..a3960fea 100644 --- a/frontend/src/screens/setup/node/LDKForm.tsx +++ b/frontend/src/screens/setup/node/LDKForm.tsx @@ -21,9 +21,7 @@ export function LDKForm() { useSetupStore.getState().updateNodeInfo({ backendType: "LDK", }); - navigate("/setup/security", { - replace: true, - }); + navigate("/setup/security"); }, [navigate, searchParams]); return ( diff --git a/frontend/src/screens/setup/node/PresetNodeForm.tsx b/frontend/src/screens/setup/node/PresetNodeForm.tsx index 39fa84e5..279823f4 100644 --- a/frontend/src/screens/setup/node/PresetNodeForm.tsx +++ b/frontend/src/screens/setup/node/PresetNodeForm.tsx @@ -24,9 +24,7 @@ export function PresetNodeForm() { }); } - navigate("/setup/security", { - replace: true, - }); + navigate("/setup/security"); }, [info, navigate, searchParams]); return ( diff --git a/frontend/src/screens/wallet/Lightning.tsx b/frontend/src/screens/wallet/Lightning.tsx index 259a5e93..cc299fd8 100644 --- a/frontend/src/screens/wallet/Lightning.tsx +++ b/frontend/src/screens/wallet/Lightning.tsx @@ -16,7 +16,7 @@ import { useChannels } from "src/hooks/useChannels"; import { useInfo } from "src/hooks/useInfo"; export default function Lightning() { - const { data: info, hasChannelManagement } = useInfo(); + const { hasChannelManagement } = useInfo(); const { data: balances } = useBalances(true); const { data: channels } = useChannels(); @@ -37,10 +37,7 @@ export default function Lightning() { balances.lightning.totalReceivableMsat < balances.lightning.totalSpendableMsat * 0.1; const showOpenFirstChannel = - hasChannelManagement && - channels && - !hasChannelsOpen && - !(info?.jitChannelsEnabled && info?.jitChannelsLiquiditySource); + hasChannelManagement && channels && !hasChannelsOpen; return ( <> diff --git a/frontend/src/screens/wallet/receive/ReceiveInvoice.tsx b/frontend/src/screens/wallet/receive/ReceiveInvoice.tsx index f7577696..cb96ba5b 100644 --- a/frontend/src/screens/wallet/receive/ReceiveInvoice.tsx +++ b/frontend/src/screens/wallet/receive/ReceiveInvoice.tsx @@ -1,30 +1,19 @@ import { - AlertTriangleIcon, ArrowLeftIcon, CopyIcon, LinkIcon, PlusIcon, ReceiptTextIcon, } from "lucide-react"; +import TickSVG from "public/images/illustrations/tick.svg"; import React from "react"; -import { Link } from "react-router"; import { toast } from "sonner"; import AppHeader from "src/components/AppHeader"; -import { CurrencyInputField } from "src/components/CurrencyInputField"; -import ExternalLink from "src/components/ExternalLink"; import { FormattedBitcoinAmount } from "src/components/FormattedBitcoinAmount"; import FormattedFiatAmount from "src/components/FormattedFiatAmount"; import Loading from "src/components/Loading"; -import LottieSuccess from "src/components/LottieSuccess"; import LowReceivingCapacityAlert from "src/components/LowReceivingCapacityAlert"; import QRCode from "src/components/QRCode"; -import { - Accordion, - AccordionContent, - AccordionItem, - AccordionTrigger, -} from "src/components/ui/accordion"; -import { Alert, AlertDescription } from "src/components/ui/alert"; import { Button } from "src/components/ui/button"; import { Card, @@ -34,19 +23,18 @@ import { CardTitle, } from "src/components/ui/card"; import { ExternalLinkButton } from "src/components/ui/custom/external-link-button"; +import { InputWithAdornment } from "src/components/ui/custom/input-with-adornment"; import { LinkButton } from "src/components/ui/custom/link-button"; import { LoadingButton } from "src/components/ui/custom/loading-button"; import { Input } from "src/components/ui/input"; import { Label } from "src/components/ui/label"; import { useAlbyMe } from "src/hooks/useAlbyMe"; import { useBalances } from "src/hooks/useBalances"; -import { useChannels } from "src/hooks/useChannels"; import { useInfo } from "src/hooks/useInfo"; import { useTransaction } from "src/hooks/useTransaction"; import { copyToClipboard } from "src/lib/clipboard"; import { cn } from "src/lib/utils"; -import ReceiveToSelect from "src/screens/wallet/receive/ReceiveToSelect"; import { CreateInvoiceRequest, Transaction } from "src/types"; import { request } from "src/utils/request"; @@ -54,60 +42,24 @@ export default function ReceiveInvoice() { const { data: info, hasChannelManagement } = useInfo(); const { data: me } = useAlbyMe(); const { data: balances } = useBalances(); - const { data: channels } = useChannels(); const [isLoading, setLoading] = React.useState(false); - const [jitChannelRequestFailed, setJitChannelRequestFailed] = - React.useState(false); const [amountSat, setAmountSat] = React.useState(""); const [description, setDescription] = React.useState(""); - const [toAppId, setToAppId] = React.useState(); const [transaction, setTransaction] = React.useState( null ); + const [paymentDone, setPaymentDone] = React.useState(false); const { data: invoiceData } = useTransaction( transaction ? transaction.paymentHash : "", true ); - const paymentDone = !!invoiceData?.settledAt; - const jitChannelsEnabled = !!info?.jitChannelsEnabled; - const configuredLsps2Source = info?.jitChannelsLiquiditySource; - const lsps2Source = jitChannelsEnabled ? configuredLsps2Source : undefined; - const lsps2MinimumPaymentSizeSat = React.useMemo(() => { - if (jitChannelsEnabled && info?.jitChannelsMinPaymentSizeMsat) { - return Math.ceil(info.jitChannelsMinPaymentSizeMsat / 1000); + + React.useEffect(() => { + if (invoiceData?.settledAt) { + setPaymentDone(true); } - return undefined; - }, [info?.jitChannelsMinPaymentSizeMsat, jitChannelsEnabled]); - // only enforce the minimum on the input when the user has no channels yet - - // their first channel must meet the minimum size. - const jitMinimumReceiveSat = channels?.length - ? undefined - : lsps2MinimumPaymentSizeSat; - const lsps2MaximumPaymentSizeSat = React.useMemo(() => { - if (jitChannelsEnabled && info?.jitChannelsMaxPaymentSizeMsat) { - return Math.floor(info.jitChannelsMaxPaymentSizeMsat / 1000); - } - return undefined; - }, [info?.jitChannelsMaxPaymentSizeMsat, jitChannelsEnabled]); - const jitMaximumReceiveSat = - hasChannelManagement && lsps2Source - ? lsps2MaximumPaymentSizeSat - : !lsps2Source && hasChannelManagement - ? balances?.lightning.totalReceivableSat - : undefined; - const totalReceivableMsat = balances?.lightning.totalReceivableMsat ?? 0; - const requestedAmountMsat = +amountSat * 1000 || transaction?.amountMsat || 0; - const isNearReceivingCapacity = - !!hasChannelManagement && requestedAmountMsat >= 0.8 * totalReceivableMsat; - const isJitReceiveInvoice = - !!hasChannelManagement && - !!lsps2Source && - !!transaction && - transaction.amountMsat > totalReceivableMsat; - const displayedJitFeeMsat = paymentDone - ? (invoiceData?.feesPaidMsat ?? 0) - : (transaction?.feesPaidMsat ?? 0); + }, [invoiceData]); if (!balances || !info || (info.albyAccountConnected && !me)) { return ; @@ -118,7 +70,6 @@ export default function ReceiveInvoice() { try { setLoading(true); - setJitChannelRequestFailed(false); const invoice = await request("/api/invoices", { method: "POST", headers: { @@ -127,7 +78,6 @@ export default function ReceiveInvoice() { body: JSON.stringify({ amountMsat: (parseInt(amountSat) || 0) * 1000, description, - toAppId, } as CreateInvoiceRequest), }); @@ -138,29 +88,9 @@ export default function ReceiveInvoice() { toast("Successfully created invoice"); } } catch (e) { - const requestedAmountSat = parseInt(amountSat) || 0; - // the user already has channels but this amount exceeds their receiving - // capacity (so a new channel is needed) and is below the LSP's minimum - // channel size - the receive may have failed because the amount was too - // small to open a second channel, so add a hint alongside the error. - const likelyTooSmallForNewChannel = - jitChannelsEnabled && - !!channels?.length && - !!lsps2MinimumPaymentSizeSat && - requestedAmountSat < lsps2MinimumPaymentSizeSat && - requestedAmountSat * 1000 > totalReceivableMsat; - let description = "" + e; - if (likelyTooSmallForNewChannel) { - description += `\n\nThis amount is over your receiving capacity and may be too small to open a new Lightning channel. Try receiving at least ${new Intl.NumberFormat().format( - lsps2MinimumPaymentSizeSat as number - )} sats, or lower the amount to fit your current capacity.`; - } toast.error("Failed to create invoice", { - description, + description: "" + e, }); - if (lsps2Source) { - setJitChannelRequestFailed(true); - } console.error(e); } finally { setLoading(false); @@ -171,19 +101,6 @@ export default function ReceiveInvoice() { copyToClipboard(transaction?.invoice as string); }; - const newChannelFeeAlert = ( -

    - Includes a {" "} - channel fee.{" "} - - Learn more - -

    - ); - return (
    - {!lsps2Source && !transaction && isNearReceivingCapacity && ( - - )} + {hasChannelManagement && + (+amountSat * 1000 || transaction?.amountMsat || 0) >= + 0.8 * balances.lightning.totalReceivableMsat && ( + + )}
    {transaction ? ( {!paymentDone ? ( <> - - -

    Waiting for Payment...

    + + +

    Waiting for payment

    - +

    - {isJitReceiveInvoice && displayedJitFeeMsat >= 1000 && ( -
    {newChannelFeeAlert}
    - )}
    - + @@ -245,7 +158,7 @@ export default function ReceiveInvoice() { - +

    - + - + Back to Wallet @@ -283,51 +197,26 @@ export default function ReceiveInvoice() {
    ) : (
    - { - if ( - jitMinimumReceiveSat && - e.currentTarget.validity.rangeUnderflow - ) { - e.currentTarget.setCustomValidity( - `You need to receive at least ${new Intl.NumberFormat().format( - jitMinimumReceiveSat - )} sats to open your first lightning channel` - ); - } else if ( - jitMaximumReceiveSat && - e.currentTarget.validity.rangeOverflow - ) { - e.currentTarget.setCustomValidity( - lsps2Source - ? `This JIT channel setup supports receiving at most ${new Intl.NumberFormat().format( - jitMaximumReceiveSat - )} sats in a single payment` - : `You can receive at most ${new Intl.NumberFormat().format( - jitMaximumReceiveSat - )} sats with your current capacity` - ); - } else { - e.currentTarget.setCustomValidity(""); +
    + + { + setAmountSat(e.target.value.trim()); + }} + min={1} + autoFocus + endAdornment={ + } - }} - maxSat={jitMaximumReceiveSat} - autoFocus - contextRows={ - hasChannelManagement && !lsps2Source && jitMaximumReceiveSat - ? [ - { - label: "Receive limit", - amountSat: jitMaximumReceiveSat, - }, - ] - : undefined - } - /> + /> +
    - {(!info?.albyAccountConnected || !me?.lightning_address) && ( - - - - View other ways to receive - - - {!info?.albyAccountConnected && info.supportsBolt12 && ( - - - Lightning Offer - - )} +
    + {!info?.albyAccountConnected && + info.backendType === "LDK" && ( - - Receive from On-chain / Other Cryptocurrency + + Lightning Offer - - - + )} + + + Receive from On-chain / Other Cryptocurrency + +
    )} )}
    - {!transaction && jitChannelRequestFailed && ( - - - - Failed to request a just-in-time channel invoice.{" "} - - Manually open a channel. - - - - )}
    {!transaction && (!info?.albyAccountConnected || !me?.lightning_address) && ( diff --git a/frontend/src/screens/wallet/receive/ReceiveOffer.tsx b/frontend/src/screens/wallet/receive/ReceiveOffer.tsx index 8685fe65..59fd08c2 100644 --- a/frontend/src/screens/wallet/receive/ReceiveOffer.tsx +++ b/frontend/src/screens/wallet/receive/ReceiveOffer.tsx @@ -91,18 +91,14 @@ export default function ReceiveOffer() { Lightning Offer - + {description && (

    {description}

    )}
    - + diff --git a/frontend/src/screens/wallet/receive/ReceiveOnchain.tsx b/frontend/src/screens/wallet/receive/ReceiveOnchain.tsx index 0044c33e..c4c378c7 100644 --- a/frontend/src/screens/wallet/receive/ReceiveOnchain.tsx +++ b/frontend/src/screens/wallet/receive/ReceiveOnchain.tsx @@ -2,11 +2,13 @@ import { useEffect, useState } from "react"; import { useNavigate } from "react-router"; import { toast } from "sonner"; import AppHeader from "src/components/AppHeader"; -import { CurrencyInputField } from "src/components/CurrencyInputField"; import { FixedFloatSwapInFlow } from "src/components/FixedFloatSwapInFlow"; +import { FormattedBitcoinAmount } from "src/components/FormattedBitcoinAmount"; +import FormattedFiatAmount from "src/components/FormattedFiatAmount"; import Loading from "src/components/Loading"; import LowReceivingCapacityAlert from "src/components/LowReceivingCapacityAlert"; import { MempoolAlert } from "src/components/MempoolAlert"; +import { InputWithAdornment } from "src/components/ui/custom/input-with-adornment"; import { LoadingButton } from "src/components/ui/custom/loading-button"; import { Label } from "src/components/ui/label"; import { RadioGroup, RadioGroupItem } from "src/components/ui/radio-group"; @@ -124,38 +126,48 @@ export default function ReceiveOnchain() { 0.8 * balances.lightning.totalReceivableMsat && ( )} - + + + : balances.lightning.totalReceivableSat * 0.99 + } + onChange={(e) => setSwapAmountSat(e.target.value)} + required + endAdornment={ + + } + /> +
    +
    +
    + Receiving Capacity:{" "} + +
    + +
    +
    +
    -
    - -
    -
    {LIGHTNING_BALANCE_LABEL}
    -
    - ); -} - -function AppOption({ app }: { app: App }) { - return ( -
    - -
    -
    {getAppDisplayName(app.name)}
    -
    -
    - ); -} - -export default function ReceiveToSelect({ appId, onChange }: Props) { - const anchorRef = useComboboxAnchor(); - const [search, setSearch] = React.useState(""); - const { data: appsData } = useApps(APP_SELECT_APPS_LIMIT, undefined, { - name: search, - }); - - const apps = React.useMemo( - () => - [...(appsData?.apps || [])].sort((a, b) => - getAppDisplayName(a.name).localeCompare(getAppDisplayName(b.name)) - ), - [appsData?.apps] - ); - - const options = React.useMemo( - () => [ - { value: LIGHTNING_BALANCE, label: LIGHTNING_BALANCE_LABEL }, - ...apps.map((app) => ({ - value: app.id.toString(), - label: getAppDisplayName(app.name), - app, - })), - ], - [apps] - ); - - const selectedOption = options.find((opt) => - appId ? opt.value === appId.toString() : undefined - ); - - return ( -
    - - option.value} - onInputValueChange={setSearch} - onValueChange={(option) => - onChange( - !option || option.value === LIGHTNING_BALANCE - ? undefined - : Number(option.value) - ) - } - > -
    - - - {selectedOption?.app ? ( - - ) : ( -
    - -
    - )} -
    -
    -
    - - No connections found. - - {(option: ReceiveToOption) => ( - - {option.app ? ( - - ) : ( - - )} - - )} - - -
    -
    - ); -} diff --git a/frontend/src/screens/wallet/send/LnurlPay.tsx b/frontend/src/screens/wallet/send/LnurlPay.tsx index cabea1f6..1d9ac2ae 100644 --- a/frontend/src/screens/wallet/send/LnurlPay.tsx +++ b/frontend/src/screens/wallet/send/LnurlPay.tsx @@ -5,11 +5,13 @@ import React from "react"; import { Link, useLocation, useNavigate } from "react-router"; import { toast } from "sonner"; import AppHeader from "src/components/AppHeader"; -import { CurrencyInputField } from "src/components/CurrencyInputField"; +import { FormattedBitcoinAmount } from "src/components/FormattedBitcoinAmount"; +import FormattedFiatAmount from "src/components/FormattedFiatAmount"; import { InsufficientLightningBalanceAlert } from "src/components/InsufficientLightningBalanceAlert"; import Loading from "src/components/Loading"; import { PaymentFailedAlert } from "src/components/PaymentFailedAlert"; import { PendingPaymentAlert } from "src/components/PendingPaymentAlert"; +import { InputWithAdornment } from "src/components/ui/custom/input-with-adornment"; import { LinkButton } from "src/components/ui/custom/link-button"; import { LoadingButton } from "src/components/ui/custom/loading-button"; import { Input } from "src/components/ui/input"; @@ -134,21 +136,42 @@ export default function LnurlPay() {

    )} - +
    + + { + setAmountSat(e.target.value.trim()); + }} + min={1} + max={balances.lightning.totalSpendableSat} + required + autoFocus + endAdornment={ + + } + /> +
    +
    +
    + Lightning Balance:{" "} + +
    + +
    +
    +
    {!!lnAddress.lnurlpData?.commentAllowed && (
    diff --git a/frontend/src/screens/wallet/send/Onchain.tsx b/frontend/src/screens/wallet/send/Onchain.tsx index 3b966137..51d1c8da 100644 --- a/frontend/src/screens/wallet/send/Onchain.tsx +++ b/frontend/src/screens/wallet/send/Onchain.tsx @@ -10,13 +10,15 @@ import { Link, useLocation, useNavigate } from "react-router"; import { toast } from "sonner"; import { AnchorReserveAlert } from "src/components/AnchorReserveAlert"; import AppHeader from "src/components/AppHeader"; -import { CurrencyInputField } from "src/components/CurrencyInputField"; import ExternalLink from "src/components/ExternalLink"; +import { FormattedBitcoinAmount } from "src/components/FormattedBitcoinAmount"; +import FormattedFiatAmount from "src/components/FormattedFiatAmount"; import { InsufficientLightningBalanceAlert } from "src/components/InsufficientLightningBalanceAlert"; import Loading from "src/components/Loading"; import { MempoolAlert } from "src/components/MempoolAlert"; import { Alert, AlertDescription, AlertTitle } from "src/components/ui/alert"; import { Button } from "src/components/ui/button"; +import { InputWithAdornment } from "src/components/ui/custom/input-with-adornment"; import { LinkButton } from "src/components/ui/custom/link-button"; import { LoadingButton } from "src/components/ui/custom/loading-button"; import { Input } from "src/components/ui/input"; @@ -188,21 +190,40 @@ function OnchainForm({ return (
    - +
    + + { + setAmountSat(e.target.value.trim()); + }} + min={ONCHAIN_DUST_SATS} + max={balances.onchain.spendableSat} + required + autoFocus + endAdornment={ + + } + /> +
    +
    + On-chain Balance:{" "} + +
    + +
    +