diff --git a/.github/.github/pull_request_template.md b/.github/.github/pull_request_template.md
new file mode 100755
index 0000000..dcf39c1
--- /dev/null
+++ b/.github/.github/pull_request_template.md
@@ -0,0 +1,9 @@
+## Goal of this PR
+
+Fixes #
+
+## Checklist
+
+- [ ] Is on the right branch
+- [ ] Documentation is up-to-date
+- [ ] Tests are up-to-date
\ No newline at end of file
diff --git a/.github/.github/workflows/ci.yml b/.github/.github/workflows/ci.yml
new file mode 100755
index 0000000..1e4b9bc
--- /dev/null
+++ b/.github/.github/workflows/ci.yml
@@ -0,0 +1,107 @@
+name: CI
+
+# Continuous integration will run whenever a pull request for the master branch
+# is created or updated.
+on:
+ workflow_dispatch:
+ pull_request:
+ branches:
+ - master
+
+jobs:
+ check:
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Check out source code
+ uses: actions/checkout@v2
+
+ - name: Check out FlowGo
+ uses: actions/checkout@v2
+ with:
+ repository: onflow/flow-go
+ ref: c0afa789365eb7a22713ed76b8de1e3efaf3a70a
+ path: flow-go
+
+ - name: Install Go
+ uses: actions/setup-go@v2
+ with:
+ go-version: 1.17
+
+ - name: Cache Go modules
+ uses: actions/cache@v2
+ with:
+ path: ~/go/pkg/mod
+ key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
+ restore-keys: |
+ ${{ runner.os }}-go-
+
+ # Here, we simply print the exact go version, to have it as part of the
+ # action's output, which might be convenient.
+ - name: Print Go version
+ run: go version
+
+ # The protobuf steps uses the official instructions to install the
+ # pre-compiled binary, see:
+ # https://grpc.io/docs/protoc-installation/#install-pre-compiled-binaries-any-os
+ - name: Install Protobuf compiler
+ run: |
+ PB_REL="https://github.com/protocolbuffers/protobuf/releases"
+ curl -LO $PB_REL/download/v3.17.3/protoc-3.17.3-linux-x86_64.zip
+ unzip protoc-3.17.3-linux-x86_64.zip -d $HOME/.local
+ export PATH="$PATH:$HOME/.local/bin"
+ git clean -fd
+
+ # In order to be able to generate the protocol buffer and GRPC files, we
+ # need to install the related Go modules.
+ - name: Install Protobuf dependencies
+ run: |
+ go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.26
+ go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.1
+ go install github.com/srikrsna/protoc-gen-gotag@v0.6.1
+
+ # Since building relic takes some time, we want to cache it.
+ - name: Cache Crypto package
+ uses: actions/cache@v2
+ with:
+ path: ./flow-go/crypto
+ key: ${{ runner.os }}-crypto
+ restore-keys: |
+ ${{ runner.os }}-crypto
+
+ # In order to be able to build with flow-go and the relic tag, we need to
+ # run its go generate target.
+ - name: Install Flow Go's crypto
+ run: |
+ cd ./flow-go/crypto
+ go generate .
+
+ # This check makes sure that the `go.mod` and `go.sum` files for Go
+ # modules are always up-to-date.
+ - name: Verify Go modules
+ run: go mod tidy && git status && git --no-pager diff && git diff-index --quiet HEAD --
+
+ # This check makes sure that the generated protocol buffer files in Go
+ # have been updated in case there was a change in the definitions.
+ - name: Verify generated files
+ run: go generate ./... && git status && git --no-pager diff && git diff-index --quiet HEAD --
+
+ # This check makes sure that the source code is formatted according to the
+ # Go standard `go fmt` formatting.
+ - name: Verify source code formatting
+ run: go fmt ./... && git status && git --no-pager diff && git diff-index --quiet HEAD --
+
+ # This check makes sure that we can compile the binary as a pure Go binary
+ # without CGO support.
+ - name: Verify compilation
+ run: go build -tags relic ./...
+
+ # This check runs all unit tests with verbose output and ensures that all
+ # of the tests pass successfully.
+ - name: Verify unit tests
+ run: go test -tags relic -v ./...
+
+ # This check runs all integration tests with verbose output and ensures
+ # that they pass successfully.
+ - name: Verify integration tests
+ run: go test -v -tags="relic integration" ./...
diff --git a/.github/.github/workflows/release.yml b/.github/.github/workflows/release.yml
new file mode 100755
index 0000000..05db0aa
--- /dev/null
+++ b/.github/.github/workflows/release.yml
@@ -0,0 +1,97 @@
+name: AutoRelease
+
+# AutoRelease will run whenever a tag is pushed.
+on:
+ workflow_dispatch:
+ push:
+ tags:
+ - '*'
+
+jobs:
+ goreleaser:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout source code
+ uses: actions/checkout@v2
+ with:
+ fetch-depth: 0
+
+ - name: Check out FlowGo
+ uses: actions/checkout@v2
+ with:
+ repository: onflow/flow-go
+ ref: c0afa789365eb7a22713ed76b8de1e3efaf3a70a
+ path: flow-go
+
+ - name: Install Go
+ uses: actions/setup-go@v2
+ with:
+ go-version: 1.17
+
+ - name: Cache Go modules
+ uses: actions/cache@v2
+ with:
+ path: ~/go/pkg/mod
+ key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
+ restore-keys: |
+ ${{ runner.os }}-go-
+
+ # Here, we simply print the exact go version, to have it as part of the
+ # action's output, which might be convenient.
+ - name: Print Go version
+ run: go version
+
+ # The protobuf steps uses the official instructions to install the
+ # pre-compiled binary, see:
+ # https://grpc.io/docs/protoc-installation/#install-pre-compiled-binaries-any-os
+ - name: Install Protobuf compiler
+ run: |
+ PB_REL="https://github.com/protocolbuffers/protobuf/releases"
+ curl -LO $PB_REL/download/v3.17.3/protoc-3.17.3-linux-x86_64.zip
+ unzip protoc-3.17.3-linux-x86_64.zip -d $HOME/.local
+ export PATH="$PATH:$HOME/.local/bin"
+ git clean -fd
+
+ # In order to be able to generate the protocol buffer and GRPC files, we
+ # need to install the related Go modules.
+ - name: Install Protobuf dependencies
+ run: |
+ go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.26
+ go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.1
+ go install github.com/srikrsna/protoc-gen-gotag@v0.6.1
+
+ # In order to be able to build with flow-go and the relic tag, we need to
+ # run its go generate target.
+ - name: Install Flow Go's crypto
+ run: |
+ cd ./flow-go/crypto
+ go generate .
+
+ # This check makes sure that the `go.mod` and `go.sum` files for Go
+ # modules are always up-to-date.
+ - name: Verify Go modules
+ run: go mod tidy && git status && git --no-pager diff && git diff-index --quiet HEAD --
+
+ # This check makes sure that the generated protocol buffer files in Go
+ # have been updated in case there was a change in the definitions.
+ - name: Generate files
+ run: go generate ./... && git status && git --no-pager diff && git diff-index --quiet HEAD --
+
+ # Install GoReleaser and print its version before running.
+ - name: Install GoReleaser
+ uses: goreleaser/goreleaser-action@v2
+ with:
+ install-only: true
+
+ - name: Show GoReleaser version
+ run: goreleaser -v
+
+ # Run GoReleaser.
+ - name: Run GoReleaser
+ uses: goreleaser/goreleaser-action@v2
+ with:
+ distribution: goreleaser
+ version: latest
+ args: release --rm-dist
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md
new file mode 100755
index 0000000..dcf39c1
--- /dev/null
+++ b/.github/pull_request_template.md
@@ -0,0 +1,9 @@
+## Goal of this PR
+
+Fixes #
+
+## Checklist
+
+- [ ] Is on the right branch
+- [ ] Documentation is up-to-date
+- [ ] Tests are up-to-date
\ No newline at end of file
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100755
index 0000000..83ff029
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,65 @@
+name: CI
+
+# Continuous integration will run whenever a pull request for the master branch
+# is created or updated.
+on:
+ workflow_dispatch:
+ pull_request:
+ branches:
+ - master
+
+jobs:
+ check:
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Check out source code
+ uses: actions/checkout@v2
+
+ - name: Install Go
+ uses: actions/setup-go@v2
+ with:
+ go-version: 1.17
+
+ - name: Cache Go modules
+ uses: actions/cache@v2
+ with:
+ path: ~/go/pkg/mod
+ key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
+ restore-keys: |
+ ${{ runner.os }}-go-
+
+ # Here, we simply print the exact go version, to have it as part of the
+ # action's output, which might be convenient.
+ - name: Print Go version
+ run: go version
+
+ # This check makes sure that the `go.mod` and `go.sum` files for Go
+ # modules are always up-to-date.
+ - name: Verify Go modules
+ run: go mod tidy && git status && git --no-pager diff && git diff-index --quiet HEAD --
+
+ # This check makes sure that the generated protocol buffer files in Go
+ # have been updated in case there was a change in the definitions.
+ - name: Verify generated files
+ run: go generate ./... && git status && git --no-pager diff && git diff-index --quiet HEAD --
+
+ # This check makes sure that the source code is formatted according to the
+ # Go standard `go fmt` formatting.
+ - name: Verify source code formatting
+ run: go fmt ./... && git status && git --no-pager diff && git diff-index --quiet HEAD --
+
+ # This check makes sure that we can compile the binary as a pure Go binary
+ # without CGO support.
+ - name: Verify compilation
+ run: go build -tags relic ./...
+
+ # This check runs all unit tests with verbose output and ensures that all
+ # of the tests pass successfully.
+ - name: Verify unit tests
+ run: go test -tags relic -v ./...
+
+ # This check runs all integration tests with verbose output and ensures
+ # that they pass successfully.
+ - name: Verify integration tests
+ run: go test -v -tags="relic integration" ./...
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
new file mode 100755
index 0000000..16351b0
--- /dev/null
+++ b/.github/workflows/release.yml
@@ -0,0 +1,64 @@
+name: AutoRelease
+
+# AutoRelease will run whenever a tag is pushed.
+on:
+ workflow_dispatch:
+ push:
+ tags:
+ - '*'
+
+jobs:
+ goreleaser:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout source code
+ uses: actions/checkout@v2
+ with:
+ fetch-depth: 0
+
+ - name: Install Go
+ uses: actions/setup-go@v2
+ with:
+ go-version: 1.17
+
+ - name: Cache Go modules
+ uses: actions/cache@v2
+ with:
+ path: ~/go/pkg/mod
+ key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
+ restore-keys: |
+ ${{ runner.os }}-go-
+
+ # Here, we simply print the exact go version, to have it as part of the
+ # action's output, which might be convenient.
+ - name: Print Go version
+ run: go version
+
+ # This check makes sure that the `go.mod` and `go.sum` files for Go
+ # modules are always up-to-date.
+ - name: Verify Go modules
+ run: go mod tidy && git status && git --no-pager diff && git diff-index --quiet HEAD --
+
+ # This check makes sure that the generated protocol buffer files in Go
+ # have been updated in case there was a change in the definitions.
+ - name: Generate files
+ run: go generate ./... && git status && git --no-pager diff && git diff-index --quiet HEAD --
+
+ # Install GoReleaser and print its version before running.
+ - name: Install GoReleaser
+ uses: goreleaser/goreleaser-action@v2
+ with:
+ install-only: true
+
+ - name: Show GoReleaser version
+ run: goreleaser -v
+
+ # Run GoReleaser.
+ - name: Run GoReleaser
+ uses: goreleaser/goreleaser-action@v2
+ with:
+ distribution: goreleaser
+ version: latest
+ args: release --rm-dist
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
diff --git a/.gitignore b/.gitignore
new file mode 100755
index 0000000..81430d3
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,4 @@
+/cmd/flow-rosetta-server/flow-rosetta-server
+*.cdc
+*.checkpoint
+*.log
diff --git a/.goreleaser.yml b/.goreleaser.yml
new file mode 100755
index 0000000..84decd4
--- /dev/null
+++ b/.goreleaser.yml
@@ -0,0 +1,28 @@
+# By default, builds only for darwin and linux, which works for us since FlowGo does not support
+# Windows builds. We also can only build on amd64 architectures since all others are also not
+# supported at the moment.
+builds:
+ - id: rosetta-server
+ binary: rosetta-server
+ main: ./cmd/flow-rosetta-server
+ goos:
+ - linux
+ goarch:
+ - amd64
+ flags:
+ - -tags=relic
+
+archives:
+ - replacements:
+ 386: i386
+ amd64: x86_64
+checksum:
+ name_template: 'checksums.txt'
+snapshot:
+ name_template: "{{ .Tag }}"
+changelog:
+ sort: asc
+ filters:
+ exclude:
+ - '^docs:'
+ - '^test:'
diff --git a/README.md b/README.md
old mode 100644
new mode 100755
diff --git a/api/rosetta/balance.go b/api/rosetta/balance.go
new file mode 100755
index 0000000..b065f9c
--- /dev/null
+++ b/api/rosetta/balance.go
@@ -0,0 +1,50 @@
+// Copyright 2021 Optakt Labs OÜ
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+package rosetta
+
+import (
+ "github.com/labstack/echo/v4"
+
+ "github.com/optakt/flow-rosetta/rosetta/request"
+ "github.com/optakt/flow-rosetta/rosetta/response"
+)
+
+// Balance implements the /account/balance endpoint of the Rosetta Data API.
+// See https://www.rosetta-api.org/docs/AccountApi.html#accountbalance
+func (d *Data) Balance(ctx echo.Context) error {
+
+ var req request.Balance
+ err := ctx.Bind(&req)
+ if err != nil {
+ return unpackError(err)
+ }
+
+ err = d.validate.Request(req)
+ if err != nil {
+ return formatError(err)
+ }
+
+ rosBlockID, balances, err := d.retrieve.Balances(req.BlockID, req.AccountID, req.Currencies)
+ if err != nil {
+ return apiError(balancesRetrieval, err)
+ }
+
+ res := response.Balance{
+ BlockID: rosBlockID,
+ Balances: balances,
+ }
+
+ return ctx.JSON(statusOK, res)
+}
diff --git a/api/rosetta/balance_integration_test.go b/api/rosetta/balance_integration_test.go
new file mode 100755
index 0000000..456b530
--- /dev/null
+++ b/api/rosetta/balance_integration_test.go
@@ -0,0 +1,561 @@
+// Copyright 2021 Optakt Labs OÜ
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+//go:build integration
+// +build integration
+
+package rosetta_test
+
+import (
+ "encoding/json"
+ "net/http"
+ "testing"
+
+ "github.com/labstack/echo/v4"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+
+ "github.com/onflow/flow-go/model/flow"
+
+ "github.com/optakt/flow-dps/models/dps"
+ "github.com/optakt/flow-rosetta/api/rosetta"
+ "github.com/optakt/flow-rosetta/rosetta/configuration"
+ "github.com/optakt/flow-rosetta/rosetta/identifier"
+ "github.com/optakt/flow-rosetta/rosetta/request"
+ "github.com/optakt/flow-rosetta/rosetta/response"
+)
+
+func TestAPI_Balance(t *testing.T) {
+ // TODO: Repair integration tests
+ // See https://github.com/optakt/flow-dps/issues/333
+ t.Skip("integration tests disabled until new snapshot is generated")
+
+ db := setupDB(t)
+ api := setupAPI(t, db)
+
+ const testAccount = "754aed9de6197641"
+
+ var (
+ zeroBlock = knownHeader(1) // block before the account appears
+ firstBlock = knownHeader(13) // block where the account first appears
+ secondBlock = knownHeader(50) // a block mid-chain
+ lastBlock = knownHeader(425) // last indexed block
+ )
+
+ tests := []struct {
+ name string
+
+ request request.Balance
+
+ wantBalance string
+ validateBlock validateBlockFunc
+ }{
+ {
+ name: "before first occurence of the account",
+ request: requestBalance(testAccount, zeroBlock),
+ wantBalance: "0",
+ validateBlock: validateBlock(t, zeroBlock.Height, zeroBlock.ID().String()),
+ },
+ {
+ name: "first occurrence of the account",
+ request: requestBalance(testAccount, firstBlock),
+ wantBalance: "10000100000",
+ validateBlock: validateBlock(t, firstBlock.Height, firstBlock.ID().String()),
+ },
+ {
+ name: "mid chain",
+ request: requestBalance(testAccount, secondBlock),
+ wantBalance: "10000099999",
+ validateBlock: validateBlock(t, secondBlock.Height, secondBlock.ID().String()),
+ },
+ {
+ name: "last indexed block",
+ request: requestBalance(testAccount, lastBlock),
+ wantBalance: "10000100002",
+ validateBlock: validateBlock(t, lastBlock.Height, lastBlock.ID().String()),
+ },
+ {
+ // Use block height only to retrieve data, but verify hash is set in the response.
+ name: "get block via height only",
+ request: request.Balance{
+ NetworkID: defaultNetwork(),
+ AccountID: identifier.Account{
+ Address: testAccount,
+ },
+ BlockID: identifier.Block{
+ Index: &secondBlock.Height,
+ },
+ Currencies: defaultCurrency(),
+ },
+
+ wantBalance: "10000099999",
+ validateBlock: validateBlock(t, secondBlock.Height, secondBlock.ID().String()),
+ },
+ {
+ name: "get latest block by omitting block identifier",
+ request: request.Balance{
+ NetworkID: defaultNetwork(),
+ AccountID: identifier.Account{
+ Address: testAccount,
+ },
+ BlockID: identifier.Block{},
+ Currencies: defaultCurrency(),
+ },
+
+ wantBalance: "10000100002",
+ validateBlock: validateBlock(t, lastBlock.Height, lastBlock.ID().String()),
+ },
+ }
+
+ for _, test := range tests {
+
+ test := test
+ t.Run(test.name, func(t *testing.T) {
+
+ t.Parallel()
+
+ rec, ctx, err := setupRecorder(balanceEndpoint, test.request)
+ require.NoError(t, err)
+
+ err = api.Balance(ctx)
+ assert.NoError(t, err)
+
+ assert.Equal(t, http.StatusOK, rec.Result().StatusCode)
+
+ var balanceResponse response.Balance
+ require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &balanceResponse))
+
+ test.validateBlock(balanceResponse.BlockID)
+
+ require.Len(t, balanceResponse.Balances, 1)
+ balance := balanceResponse.Balances[0]
+
+ assert.Equal(t, test.request.Currencies[0].Symbol, balance.Currency.Symbol)
+ assert.Equal(t, test.request.Currencies[0].Decimals, balance.Currency.Decimals)
+ assert.Equal(t, test.wantBalance, balance.Value)
+ })
+ }
+}
+
+func TestAPI_BalanceHandlesErrors(t *testing.T) {
+ // TODO: Repair integration tests
+ // See https://github.com/optakt/flow-dps/issues/333
+ t.Skip("integration tests disabled until new snapshot is generated")
+
+ db := setupDB(t)
+ api := setupAPI(t, db)
+
+ // Defined valid balance request fields.
+ var (
+ testAccount = identifier.Account{Address: "754aed9de6197641"}
+ testHeight uint64 = 13
+ lastHeight uint64 = 425
+
+ testBlock = identifier.Block{
+ Index: &testHeight,
+ Hash: "af528bb047d6cd1400a326bb127d689607a096f5ccd81d8903dfebbac26afb23",
+ }
+ )
+
+ const (
+ invalidAddress = "0000000000000000" // valid 16-digit hex value but not a valid account ID
+
+ trimmedBlockHash = "af528bb047d6cd1400a326bb127d689607a096f5ccd81d8903dfebbac26afb2" // block hash a character short
+
+ trimmedAddress = "754aed9de619764" // account ID a character short
+ invalidAddressHex = "754aed9de619764z" // invalid hex string
+
+ accFirstOccurrence = 13
+ )
+
+ tests := []struct {
+ name string
+
+ request request.Balance
+
+ checkError assert.ErrorAssertionFunc
+ }{
+ {
+ name: "empty balance request",
+ request: request.Balance{},
+
+ checkError: checkRosettaError(http.StatusBadRequest, configuration.ErrorInvalidFormat),
+ },
+ {
+ name: "missing blockchain name",
+ request: request.Balance{
+ NetworkID: identifier.Network{
+ Blockchain: "",
+ Network: dps.FlowTestnet.String(),
+ },
+ AccountID: testAccount,
+ BlockID: testBlock,
+ Currencies: defaultCurrency(),
+ },
+
+ checkError: checkRosettaError(http.StatusBadRequest, configuration.ErrorInvalidFormat),
+ },
+ {
+ name: "invalid blockchain name",
+ request: request.Balance{
+ NetworkID: identifier.Network{
+ Blockchain: invalidBlockchain,
+ Network: dps.FlowTestnet.String(),
+ },
+ AccountID: testAccount,
+ BlockID: testBlock,
+ Currencies: defaultCurrency(),
+ },
+
+ checkError: checkRosettaError(http.StatusUnprocessableEntity, configuration.ErrorInvalidNetwork),
+ },
+ {
+ name: "missing network name",
+ request: request.Balance{
+ NetworkID: identifier.Network{
+ Blockchain: dps.FlowBlockchain,
+ Network: "",
+ },
+ AccountID: testAccount,
+ BlockID: testBlock,
+ Currencies: defaultCurrency(),
+ },
+ checkError: checkRosettaError(http.StatusBadRequest, configuration.ErrorInvalidFormat),
+ },
+ {
+ name: "invalid network name",
+ request: request.Balance{
+ NetworkID: identifier.Network{
+ Blockchain: dps.FlowBlockchain,
+ Network: invalidNetwork,
+ },
+ AccountID: testAccount,
+ BlockID: testBlock,
+ Currencies: defaultCurrency(),
+ },
+
+ checkError: checkRosettaError(http.StatusUnprocessableEntity, configuration.ErrorInvalidNetwork),
+ },
+ {
+ name: "invalid length of block id",
+ request: request.Balance{
+ NetworkID: defaultNetwork(),
+ AccountID: testAccount,
+ BlockID: identifier.Block{Index: &testHeight, Hash: trimmedBlockHash},
+ Currencies: defaultCurrency(),
+ },
+
+ checkError: checkRosettaError(http.StatusBadRequest, configuration.ErrorInvalidFormat),
+ },
+ {
+ name: "missing account address",
+ request: request.Balance{
+ NetworkID: defaultNetwork(),
+ AccountID: identifier.Account{Address: ""},
+ BlockID: testBlock,
+ Currencies: defaultCurrency(),
+ },
+
+ checkError: checkRosettaError(http.StatusBadRequest, configuration.ErrorInvalidFormat),
+ },
+ {
+ name: "invalid length of account address",
+ request: request.Balance{
+ NetworkID: defaultNetwork(),
+ AccountID: identifier.Account{Address: trimmedAddress},
+ BlockID: testBlock,
+ Currencies: defaultCurrency(),
+ },
+
+ checkError: checkRosettaError(http.StatusBadRequest, configuration.ErrorInvalidFormat),
+ },
+ {
+ name: "missing currency data",
+ request: request.Balance{
+ NetworkID: defaultNetwork(),
+ AccountID: testAccount,
+ BlockID: testBlock,
+ Currencies: []identifier.Currency{},
+ },
+
+ checkError: checkRosettaError(http.StatusBadRequest, configuration.ErrorInvalidFormat),
+ },
+ {
+ name: "missing currency symbol",
+ request: request.Balance{
+ NetworkID: defaultNetwork(),
+ AccountID: testAccount,
+ BlockID: testBlock,
+ Currencies: []identifier.Currency{{Symbol: "", Decimals: 8}},
+ },
+
+ checkError: checkRosettaError(http.StatusBadRequest, configuration.ErrorInvalidFormat),
+ },
+ {
+ name: "some currency symbols missing",
+ request: request.Balance{
+ NetworkID: defaultNetwork(),
+ AccountID: testAccount,
+ BlockID: testBlock,
+ Currencies: []identifier.Currency{
+ {Symbol: dps.FlowSymbol, Decimals: 8},
+ {Symbol: "", Decimals: 8},
+ {Symbol: dps.FlowSymbol, Decimals: 8},
+ },
+ },
+
+ checkError: checkRosettaError(http.StatusBadRequest, configuration.ErrorInvalidFormat),
+ },
+ {
+ name: "missing block height",
+ request: request.Balance{
+ NetworkID: defaultNetwork(),
+ AccountID: testAccount,
+ BlockID: identifier.Block{Index: nil, Hash: "af528bb047d6cd1400a326bb127d689607a096f5ccd81d8903dfebbac26afb23"},
+ Currencies: defaultCurrency(),
+ },
+
+ checkError: checkRosettaError(http.StatusInternalServerError, configuration.ErrorInternal),
+ },
+ {
+ name: "invalid block hash",
+ request: request.Balance{
+ NetworkID: defaultNetwork(),
+ AccountID: testAccount,
+ BlockID: identifier.Block{Index: &testHeight, Hash: invalidBlockHash},
+ Currencies: defaultCurrency(),
+ },
+
+ checkError: checkRosettaError(http.StatusUnprocessableEntity, configuration.ErrorInvalidBlock),
+ },
+ {
+ name: "unkown block requested",
+ request: request.Balance{
+ NetworkID: defaultNetwork(),
+ AccountID: testAccount,
+ BlockID: identifier.Block{Index: getUint64P(lastHeight + 1)},
+ Currencies: defaultCurrency(),
+ },
+
+ checkError: checkRosettaError(http.StatusUnprocessableEntity, configuration.ErrorUnknownBlock),
+ },
+ {
+ name: "mismatched block id and height",
+ request: request.Balance{
+ NetworkID: defaultNetwork(),
+ AccountID: testAccount,
+ BlockID: identifier.Block{Index: &testHeight, Hash: "9035c558379b208eba11130c928537fe50ad93cdee314980fccb695aa31df7fc"},
+ Currencies: defaultCurrency(),
+ },
+
+ checkError: checkRosettaError(http.StatusUnprocessableEntity, configuration.ErrorInvalidBlock),
+ },
+ {
+ name: "invalid account ID hex",
+ request: request.Balance{
+ NetworkID: defaultNetwork(),
+ AccountID: identifier.Account{Address: invalidAddressHex},
+ BlockID: testBlock,
+ Currencies: defaultCurrency(),
+ },
+
+ checkError: checkRosettaError(http.StatusUnprocessableEntity, configuration.ErrorInvalidAccount),
+ },
+ {
+ name: "invalid account ID",
+ request: request.Balance{
+ NetworkID: defaultNetwork(),
+ AccountID: identifier.Account{Address: invalidAddress},
+ BlockID: testBlock,
+ Currencies: defaultCurrency(),
+ },
+
+ checkError: checkRosettaError(http.StatusUnprocessableEntity, configuration.ErrorInvalidAccount),
+ },
+ {
+ name: "unknown currency requested",
+ request: request.Balance{
+ NetworkID: defaultNetwork(),
+ AccountID: testAccount,
+ BlockID: testBlock,
+ Currencies: []identifier.Currency{{Symbol: invalidToken, Decimals: 8}},
+ },
+
+ checkError: checkRosettaError(http.StatusUnprocessableEntity, configuration.ErrorUnknownCurrency),
+ },
+ {
+ name: "invalid currency decimal count",
+ request: request.Balance{
+ NetworkID: defaultNetwork(),
+ AccountID: testAccount,
+ BlockID: testBlock,
+ Currencies: []identifier.Currency{{Symbol: dps.FlowSymbol, Decimals: 7}},
+ },
+
+ checkError: checkRosettaError(http.StatusUnprocessableEntity, configuration.ErrorInvalidCurrency),
+ },
+ {
+ name: "invalid currency decimal count in a list of currencies",
+ request: request.Balance{
+ NetworkID: defaultNetwork(),
+ AccountID: testAccount,
+ BlockID: testBlock,
+ Currencies: []identifier.Currency{
+ {Symbol: dps.FlowSymbol, Decimals: dps.FlowDecimals},
+ {Symbol: dps.FlowSymbol, Decimals: 7},
+ {Symbol: dps.FlowSymbol, Decimals: dps.FlowDecimals},
+ },
+ },
+
+ checkError: checkRosettaError(http.StatusUnprocessableEntity, configuration.ErrorInvalidCurrency),
+ },
+ }
+
+ for _, test := range tests {
+
+ test := test
+ t.Run(test.name, func(t *testing.T) {
+
+ t.Parallel()
+
+ _, ctx, err := setupRecorder(balanceEndpoint, test.request)
+ require.NoError(t, err)
+
+ // Execute the request.
+ err = api.Balance(ctx)
+ test.checkError(t, err)
+ })
+ }
+}
+
+// TestAPI_BalanceHandlesMalformedRequest tests whether an improper JSON (e.g. wrong field types) results in a '400 Bad Request' error.
+func TestAPI_BalanceHandlesMalformedRequest(t *testing.T) {
+ // TODO: Repair integration tests
+ // See https://github.com/optakt/flow-dps/issues/333
+ t.Skip("integration tests disabled until new snapshot is generated")
+
+ db := setupDB(t)
+ api := setupAPI(t, db)
+
+ const (
+ wrongFieldType = `{
+ "network_identifier": {
+ "blockchain": "flow",
+ "network": 99
+ }
+ }`
+
+ unclosedBracket = `{
+ "network_identifier": {
+ "blockchain" : "flow",
+ "network" : "flow-testnet"
+ },
+ "block_identifier" : {
+ "index" : 13,
+ "hash" : "af528bb047d6cd1400a326bb127d689607a096f5ccd81d8903dfebbac26afb23"
+ },
+ "account_identifier" : {
+ "address" : "754aed9de6197641"
+ },
+ "currencies" : [
+ { "symbol" : "FLOW" , "decimals" : 8 }
+ ]`
+
+ validJSON = `{
+ "network_identifier": {
+ "blockchain" : "flow",
+ "network" : "flow-testnet"
+ },
+ "block_identifier" : {
+ "index" : 13,
+ "hash" : "af528bb047d6cd1400a326bb127d689607a096f5ccd81d8903dfebbac26afb23"
+ },
+ "account_identifier" : {
+ "address" : "754aed9de6197641"
+ },
+ "currencies" : [
+ { "symbol" : "FLOW" , "decimals" : 8 }
+ ]
+ }`
+ )
+
+ tests := []struct {
+ name string
+ payload []byte
+ prepare func(req *http.Request)
+ }{
+ {
+ name: "wrong field type",
+ payload: []byte(wrongFieldType),
+ prepare: func(req *http.Request) {
+ req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON)
+ },
+ },
+ {
+ name: "unclosed bracket",
+ payload: []byte(unclosedBracket),
+ prepare: func(req *http.Request) {
+ req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON)
+ },
+ },
+ {
+ name: "valid payload with no MIME type set",
+ payload: []byte(validJSON),
+ prepare: func(req *http.Request) {
+ req.Header.Set(echo.HeaderContentType, "")
+ },
+ },
+ }
+
+ for _, test := range tests {
+
+ test := test
+ t.Run(test.name, func(t *testing.T) {
+
+ t.Parallel()
+
+ _, ctx, err := setupRecorder(balanceEndpoint, test.payload, test.prepare)
+ require.NoError(t, err)
+
+ err = api.Balance(ctx)
+
+ assert.Error(t, err)
+
+ echoErr, ok := err.(*echo.HTTPError)
+ require.True(t, ok)
+
+ assert.Equal(t, http.StatusBadRequest, echoErr.Code)
+ gotErr, ok := echoErr.Message.(rosetta.Error)
+ require.True(t, ok)
+
+ assert.Equal(t, configuration.ErrorInvalidEncoding, gotErr.ErrorDefinition)
+ })
+ }
+}
+
+// requestBalance generates a BalanceRequest with the specified parameters.
+func requestBalance(address string, header flow.Header) request.Balance {
+
+ return request.Balance{
+ NetworkID: defaultNetwork(),
+ AccountID: identifier.Account{
+ Address: address,
+ },
+ BlockID: identifier.Block{
+ Index: &header.Height,
+ Hash: header.ID().String(),
+ },
+ Currencies: defaultCurrency(),
+ }
+}
diff --git a/api/rosetta/block.go b/api/rosetta/block.go
new file mode 100755
index 0000000..97878fa
--- /dev/null
+++ b/api/rosetta/block.go
@@ -0,0 +1,50 @@
+// Copyright 2021 Optakt Labs OÜ
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+package rosetta
+
+import (
+ "github.com/labstack/echo/v4"
+
+ "github.com/optakt/flow-rosetta/rosetta/request"
+ "github.com/optakt/flow-rosetta/rosetta/response"
+)
+
+// Block implements the /block endpoint of the Rosetta Data API.
+// See https://www.rosetta-api.org/docs/BlockApi.html#block
+func (d *Data) Block(ctx echo.Context) error {
+
+ var req request.Block
+ err := ctx.Bind(&req)
+ if err != nil {
+ return unpackError(err)
+ }
+
+ err = d.validate.Request(req)
+ if err != nil {
+ return formatError(err)
+ }
+
+ block, extraTxIDs, err := d.retrieve.Block(req.BlockID)
+ if err != nil {
+ return apiError(blockRetrieval, err)
+ }
+
+ res := response.Block{
+ Block: block,
+ OtherTransactions: extraTxIDs,
+ }
+
+ return ctx.JSON(statusOK, res)
+}
diff --git a/api/rosetta/block_integration_test.go b/api/rosetta/block_integration_test.go
new file mode 100755
index 0000000..914f4bd
--- /dev/null
+++ b/api/rosetta/block_integration_test.go
@@ -0,0 +1,529 @@
+// Copyright 2021 Optakt Labs OÜ
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+//go:build integration
+// +build integration
+
+package rosetta_test
+
+import (
+ "encoding/json"
+ "net/http"
+ "strconv"
+ "testing"
+
+ "github.com/labstack/echo/v4"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+
+ "github.com/onflow/flow-go/model/flow"
+
+ "github.com/optakt/flow-dps/models/convert"
+ "github.com/optakt/flow-dps/models/dps"
+ "github.com/optakt/flow-rosetta/api/rosetta"
+ "github.com/optakt/flow-rosetta/rosetta/configuration"
+ "github.com/optakt/flow-rosetta/rosetta/identifier"
+ "github.com/optakt/flow-rosetta/rosetta/object"
+ "github.com/optakt/flow-rosetta/rosetta/request"
+ "github.com/optakt/flow-rosetta/rosetta/response"
+)
+
+type validateBlockFunc func(identifier.Block)
+type validateTxFunc func([]*object.Transaction)
+
+func TestAPI_Block(t *testing.T) {
+ // TODO: Repair integration tests
+ // See https://github.com/optakt/flow-dps/issues/333
+ t.Skip("integration tests disabled until new snapshot is generated")
+
+ db := setupDB(t)
+ api := setupAPI(t, db)
+
+ // Headers of known blocks to verify.
+ var (
+ firstHeader = knownHeader(0)
+ secondHeader = knownHeader(1)
+ midHeader1 = knownHeader(13)
+ midHeader2 = knownHeader(43)
+ midHeader3 = knownHeader(44)
+ lastHeader = knownHeader(425) // header of last indexed block
+ )
+
+ const (
+ rootAccount = "8c5303eaa26202d6"
+ senderAccount = "754aed9de6197641"
+ receiverAccount = "631e88ae7f1d7c20"
+
+ initialLoadTx = "a9c9ab28ea76b7dbfd1f2666f74348e4188d67cf68248df6634cee3f06adf7b1"
+ transferTx = "d5c18baf6c8d11f0693e71dbb951c4856d4f25a456f4d5285a75fd73af39161c"
+ )
+
+ tests := []struct {
+ name string
+
+ request request.Block
+
+ wantTimestamp int64
+ wantParentHash string
+ wantParentHeight uint64
+ validateTransactions validateTxFunc
+ validateBlock validateBlockFunc
+ }{
+ {
+ // First block. Besides the standard validation, it's also a special case
+ // since according to the Rosetta spec, it should point to itself as the parent.
+ name: "first block",
+ request: blockRequest(firstHeader),
+
+ wantTimestamp: convert.RosettaTime(firstHeader.Timestamp),
+ wantParentHash: firstHeader.ID().String(),
+ wantParentHeight: firstHeader.Height,
+ validateBlock: validateByHeader(t, firstHeader),
+ },
+ {
+ name: "child of first block",
+ request: blockRequest(secondHeader),
+
+ wantTimestamp: convert.RosettaTime(secondHeader.Timestamp),
+ wantParentHash: secondHeader.ParentID.String(),
+ wantParentHeight: secondHeader.Height - 1,
+ validateBlock: validateByHeader(t, secondHeader),
+ },
+ {
+ // Initial transfer of currency from the root account to the user - 100 tokens.
+ name: "block mid-chain with transactions",
+ request: blockRequest(midHeader1),
+
+ wantTimestamp: convert.RosettaTime(midHeader1.Timestamp),
+ wantParentHash: midHeader1.ParentID.String(),
+ wantParentHeight: midHeader1.Height - 1,
+ validateBlock: validateByHeader(t, midHeader1),
+ validateTransactions: validateTransfer(t, initialLoadTx, rootAccount, senderAccount, 100_00000000),
+ },
+ {
+ name: "block mid-chain without transactions",
+ request: blockRequest(midHeader2),
+
+ wantTimestamp: convert.RosettaTime(midHeader2.Timestamp),
+ wantParentHash: midHeader2.ParentID.String(),
+ wantParentHeight: midHeader2.Height - 1,
+ validateBlock: validateByHeader(t, midHeader2),
+ },
+ {
+ // Transaction between two users.
+ name: "second block mid-chain with transactions",
+ request: blockRequest(midHeader3),
+
+ wantTimestamp: convert.RosettaTime(midHeader3.Timestamp),
+ wantParentHash: midHeader3.ParentID.String(),
+ wantParentHeight: midHeader3.Height - 1,
+ validateBlock: validateByHeader(t, midHeader3),
+ validateTransactions: validateTransfer(t, transferTx, senderAccount, receiverAccount, 1),
+ },
+ {
+ name: "lookup of a block mid-chain by index only",
+ request: request.Block{
+ NetworkID: defaultNetwork(),
+ BlockID: identifier.Block{Index: &midHeader3.Height},
+ },
+
+ wantTimestamp: convert.RosettaTime(midHeader3.Timestamp),
+ wantParentHash: midHeader3.ParentID.String(),
+ wantParentHeight: midHeader3.Height - 1,
+ validateTransactions: validateTransfer(t, transferTx, senderAccount, receiverAccount, 1),
+ validateBlock: validateBlock(t, midHeader3.Height, midHeader3.ID().String()), // verify that the returned block ID has both height and hash
+ },
+ {
+ name: "last indexed block",
+ request: blockRequest(lastHeader),
+
+ wantTimestamp: convert.RosettaTime(lastHeader.Timestamp),
+ wantParentHash: lastHeader.ParentID.String(),
+ wantParentHeight: lastHeader.Height - 1,
+ validateBlock: validateByHeader(t, lastHeader),
+ },
+ {
+ name: "last indexed block by omitting block identifier",
+ request: request.Block{
+ NetworkID: defaultNetwork(),
+ BlockID: identifier.Block{},
+ },
+
+ wantTimestamp: convert.RosettaTime(lastHeader.Timestamp),
+ wantParentHash: lastHeader.ParentID.String(),
+ wantParentHeight: lastHeader.Height - 1,
+ validateBlock: validateByHeader(t, lastHeader),
+ },
+ }
+
+ for _, test := range tests {
+
+ test := test
+ t.Run(test.name, func(t *testing.T) {
+
+ t.Parallel()
+
+ rec, ctx, err := setupRecorder(blockEndpoint, test.request)
+ require.NoError(t, err)
+
+ err = api.Block(ctx)
+ assert.NoError(t, err)
+
+ var blockResponse response.Block
+ require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &blockResponse))
+ require.NotNil(t, blockResponse.Block)
+
+ test.validateBlock(blockResponse.Block.ID)
+
+ assert.Equal(t, test.wantTimestamp, blockResponse.Block.Timestamp)
+
+ // Verify that the information about the parent block (index and hash) is correct.
+ assert.Equal(t, test.wantParentHash, blockResponse.Block.ParentID.Hash)
+
+ if test.validateTransactions != nil {
+ test.validateTransactions(blockResponse.Block.Transactions)
+ }
+
+ require.NotNil(t, blockResponse.Block.ParentID.Index)
+ assert.Equal(t, test.wantParentHeight, *blockResponse.Block.ParentID.Index)
+ })
+ }
+}
+
+func TestAPI_BlockHandlesErrors(t *testing.T) {
+ // TODO: Repair integration tests
+ // See https://github.com/optakt/flow-dps/issues/333
+ t.Skip("integration tests disabled until new snapshot is generated")
+
+ db := setupDB(t)
+ api := setupAPI(t, db)
+
+ var (
+ validBlockHeight uint64 = 44
+ lastHeight uint64 = 425
+
+ validBlockHash = knownHeader(validBlockHeight).ID().String()
+ )
+
+ const trimmedBlockHash = "dab186b45199c0c26060ea09288b2f16032da40fc54c81bb2a8267a5c13906e" // blockID a character too short
+
+ var validBlockID = identifier.Block{
+ Index: &validBlockHeight,
+ Hash: validBlockHash,
+ }
+
+ tests := []struct {
+ name string
+
+ request request.Block
+
+ checkErr assert.ErrorAssertionFunc
+ }{
+ {
+ // Effectively the same as the 'missing blockchain name' test case, since it's the first validation step.
+ name: "empty block request",
+ request: request.Block{},
+
+ checkErr: checkRosettaError(http.StatusBadRequest, configuration.ErrorInvalidFormat),
+ },
+ {
+ name: "missing blockchain name",
+ request: request.Block{
+ NetworkID: identifier.Network{
+ Blockchain: "",
+ Network: dps.FlowTestnet.String(),
+ },
+ BlockID: validBlockID,
+ },
+
+ checkErr: checkRosettaError(http.StatusBadRequest, configuration.ErrorInvalidFormat),
+ },
+ {
+ name: "invalid blockchain name",
+ request: request.Block{
+ NetworkID: identifier.Network{
+ Blockchain: invalidBlockchain,
+ Network: dps.FlowTestnet.String(),
+ },
+ BlockID: validBlockID,
+ },
+
+ checkErr: checkRosettaError(http.StatusUnprocessableEntity, configuration.ErrorInvalidNetwork),
+ },
+ {
+ name: "missing network name",
+ request: request.Block{
+ NetworkID: identifier.Network{
+ Blockchain: dps.FlowBlockchain,
+ Network: "",
+ },
+ BlockID: validBlockID,
+ },
+
+ checkErr: checkRosettaError(http.StatusBadRequest, configuration.ErrorInvalidFormat),
+ },
+ {
+ name: "invalid network name",
+ request: request.Block{
+ NetworkID: identifier.Network{
+ Blockchain: dps.FlowBlockchain,
+ Network: invalidNetwork,
+ },
+ BlockID: validBlockID,
+ },
+
+ checkErr: checkRosettaError(http.StatusUnprocessableEntity, configuration.ErrorInvalidNetwork),
+ },
+ {
+ name: "invalid length of block id",
+ request: request.Block{
+ NetworkID: defaultNetwork(),
+ BlockID: identifier.Block{
+ Index: getUint64P(43),
+ Hash: trimmedBlockHash,
+ },
+ },
+
+ checkErr: checkRosettaError(http.StatusBadRequest, configuration.ErrorInvalidFormat),
+ },
+ {
+ name: "missing block height",
+ request: request.Block{
+ NetworkID: defaultNetwork(),
+ BlockID: identifier.Block{
+ Hash: validBlockHash,
+ },
+ },
+
+ checkErr: checkRosettaError(http.StatusInternalServerError, configuration.ErrorInternal),
+ },
+ {
+ name: "invalid block hash",
+ request: request.Block{
+ NetworkID: defaultNetwork(),
+ BlockID: identifier.Block{
+ Index: getUint64P(13),
+ Hash: invalidBlockHash,
+ },
+ },
+
+ checkErr: checkRosettaError(http.StatusUnprocessableEntity, configuration.ErrorInvalidBlock),
+ },
+ {
+ name: "unknown block",
+ request: request.Block{
+ NetworkID: defaultNetwork(),
+ BlockID: identifier.Block{
+ Index: getUint64P(lastHeight + 1),
+ },
+ },
+
+ checkErr: checkRosettaError(http.StatusUnprocessableEntity, configuration.ErrorUnknownBlock),
+ },
+ {
+ name: "mismatched block height and hash",
+ request: request.Block{
+ NetworkID: defaultNetwork(),
+ BlockID: identifier.Block{
+ Index: getUint64P(validBlockHeight - 1),
+ Hash: validBlockHash,
+ },
+ },
+
+ checkErr: checkRosettaError(http.StatusUnprocessableEntity, configuration.ErrorInvalidBlock),
+ },
+ }
+
+ for _, test := range tests {
+ test := test
+
+ t.Run(test.name, func(t *testing.T) {
+
+ t.Parallel()
+
+ _, ctx, err := setupRecorder(blockEndpoint, test.request)
+ require.NoError(t, err)
+
+ err = api.Block(ctx)
+ test.checkErr(t, err)
+ })
+ }
+}
+
+func TestAPI_BlockHandlesMalformedRequest(t *testing.T) {
+ // TODO: Repair integration tests
+ // See https://github.com/optakt/flow-dps/issues/333
+ t.Skip("integration tests disabled until new snapshot is generated")
+
+ db := setupDB(t)
+ api := setupAPI(t, db)
+
+ const (
+ // Network field is an integer instead of a string.
+ wrongFieldType = `
+ {
+ "network_identifier": {
+ "blockchain": "flow",
+ "network": 99
+ }
+ }`
+
+ unclosedBracket = `
+ {
+ "network_identifier": {
+ "blockchain": "flow",
+ "network": "flow-testnet"
+ },
+ "block_identifier": {
+ "index": 13,
+ "hash": "af528bb047d6cd1400a326bb127d689607a096f5ccd81d8903dfebbac26afb23"
+ }`
+
+ validJSON = `
+ {
+ "network_identifier": {
+ "blockchain": "flow",
+ "network": "flow-testnet"
+ },
+ "block_identifier": {
+ "index": 13,
+ "hash": "af528bb047d6cd1400a326bb127d689607a096f5ccd81d8903dfebbac26afb23"
+ }
+ }`
+ )
+
+ tests := []struct {
+ name string
+ payload []byte
+ prepare func(*http.Request)
+ }{
+ {
+ name: "wrong field type",
+ payload: []byte(wrongFieldType),
+ prepare: func(req *http.Request) {
+ req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON)
+ },
+ },
+ {
+ name: "unclosed bracket",
+ payload: []byte(unclosedBracket),
+ prepare: func(req *http.Request) {
+ req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON)
+ },
+ },
+ {
+ name: "valid payload with no MIME type set",
+ payload: []byte(validJSON),
+ prepare: func(req *http.Request) {
+ req.Header.Set(echo.HeaderContentType, "")
+ },
+ },
+ }
+
+ for _, test := range tests {
+ test := test
+
+ t.Run(test.name, func(t *testing.T) {
+
+ t.Parallel()
+
+ _, ctx, err := setupRecorder(blockEndpoint, test.payload, test.prepare)
+ require.NoError(t, err)
+
+ err = api.Block(ctx)
+ assert.Error(t, err)
+
+ echoErr, ok := err.(*echo.HTTPError)
+ require.True(t, ok)
+
+ assert.Equal(t, http.StatusBadRequest, echoErr.Code)
+
+ gotErr, ok := echoErr.Message.(rosetta.Error)
+ require.True(t, ok)
+
+ assert.Equal(t, configuration.ErrorInvalidEncoding, gotErr.ErrorDefinition)
+ assert.NotEmpty(t, gotErr.Description)
+ })
+ }
+
+}
+
+// blockRequest generates a BlockRequest with the specified parameters.
+func blockRequest(header flow.Header) request.Block {
+
+ return request.Block{
+ NetworkID: defaultNetwork(),
+ BlockID: identifier.Block{
+ Index: &header.Height,
+ Hash: header.ID().String(),
+ },
+ }
+}
+
+func validateTransfer(t *testing.T, hash string, from string, to string, amount int64) validateTxFunc {
+
+ t.Helper()
+
+ return func(transactions []*object.Transaction) {
+
+ require.Len(t, transactions, 1)
+
+ tx := transactions[0]
+
+ assert.Equal(t, tx.ID.Hash, hash)
+ assert.Equal(t, len(tx.Operations), 2)
+
+ // Operations come in pairs. A negative transfer of funds for the sender and a positive one for the receiver.
+ require.Equal(t, len(tx.Operations), 2)
+
+ op1 := tx.Operations[0]
+ op2 := tx.Operations[1]
+
+ assert.Equal(t, op1.Type, dps.OperationTransfer)
+ assert.Equal(t, op1.Status, dps.StatusCompleted)
+
+ assert.Equal(t, op1.Amount.Currency.Symbol, dps.FlowSymbol)
+ assert.Equal(t, op1.Amount.Currency.Decimals, uint(dps.FlowDecimals))
+
+ address := op1.AccountID.Address
+ if address != from && address != to {
+ t.Errorf("unexpected account address (%v)", address)
+ }
+
+ wantValue := strconv.FormatInt(amount, 10)
+ if address == from {
+ wantValue = "-" + wantValue
+ }
+
+ assert.Equal(t, op1.Amount.Value, wantValue)
+
+ assert.Equal(t, op2.Type, dps.OperationTransfer)
+ assert.Equal(t, op2.Status, dps.StatusCompleted)
+
+ assert.Equal(t, op2.Amount.Currency.Symbol, dps.FlowSymbol)
+ assert.Equal(t, op2.Amount.Currency.Decimals, uint(dps.FlowDecimals))
+
+ address = op2.AccountID.Address
+ if address != from && address != to {
+ t.Errorf("unexpected account address (%v)", address)
+ }
+
+ wantValue = strconv.FormatInt(amount, 10)
+ if address == from {
+ wantValue = "-" + wantValue
+ }
+
+ assert.Equal(t, op2.Amount.Value, wantValue)
+ }
+}
diff --git a/api/rosetta/combine.go b/api/rosetta/combine.go
new file mode 100755
index 0000000..fb8213d
--- /dev/null
+++ b/api/rosetta/combine.go
@@ -0,0 +1,51 @@
+// Copyright 2021 Optakt Labs OÜ
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+package rosetta
+
+import (
+ "github.com/labstack/echo/v4"
+
+ "github.com/optakt/flow-rosetta/rosetta/request"
+ "github.com/optakt/flow-rosetta/rosetta/response"
+)
+
+// Combine implements the /construction/combine endpoint of the Rosetta Construction API.
+// It creates a signed transaction by combining an unsigned transaction and
+// a list of signatures.
+// See https://www.rosetta-api.org/docs/ConstructionApi.html#constructioncombine
+func (c *Construction) Combine(ctx echo.Context) error {
+
+ var req request.Combine
+ err := ctx.Bind(&req)
+ if err != nil {
+ return unpackError(err)
+ }
+
+ err = c.validate.Request(req)
+ if err != nil {
+ return formatError(err)
+ }
+
+ signed, err := c.transact.AttachSignatures(req.UnsignedTransaction, req.Signatures)
+ if err != nil {
+ return apiError(txSigning, err)
+ }
+
+ res := response.Combine{
+ SignedTransaction: signed,
+ }
+
+ return ctx.JSON(statusOK, res)
+}
diff --git a/api/rosetta/configuration.go b/api/rosetta/configuration.go
new file mode 100755
index 0000000..faf817f
--- /dev/null
+++ b/api/rosetta/configuration.go
@@ -0,0 +1,32 @@
+// Copyright 2021 Optakt Labs OÜ
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+package rosetta
+
+import (
+ "github.com/optakt/flow-rosetta/rosetta/identifier"
+ "github.com/optakt/flow-rosetta/rosetta/meta"
+)
+
+// Configuration represents the configuration parameters of a particular blockchain from
+// the Rosetta API's perspective. It details some blockchain metadata, its supported operations,
+// errors, and more.
+// See https://www.rosetta-api.org/docs/NetworkApi.html#networkoptions
+type Configuration interface {
+ Network() identifier.Network
+ Version() meta.Version
+ Operations() []string
+ Statuses() []meta.StatusDefinition
+ Errors() []meta.ErrorDefinition
+}
diff --git a/api/rosetta/construction.go b/api/rosetta/construction.go
new file mode 100755
index 0000000..c519517
--- /dev/null
+++ b/api/rosetta/construction.go
@@ -0,0 +1,42 @@
+// Copyright 2021 Optakt Labs OÜ
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+package rosetta
+
+// Construction implements the Rosetta Construction API specification.
+// See https://www.rosetta-api.org/docs/construction_api_introduction.html
+type Construction struct {
+ config Configuration
+ transact Transactor
+ validate Validator
+
+ // Retrieve is used to get the latest block ID. This is needed since
+ // transactions require a reference block ID, so that their validity
+ // or expiration can be determined.
+ retrieve Retriever
+}
+
+// NewConstruction creates a new instance of the Construction API using the given configuration
+// to handle transaction construction requests.
+func NewConstruction(config Configuration, transact Transactor, retrieve Retriever, validate Validator) *Construction {
+
+ c := Construction{
+ config: config,
+ transact: transact,
+ retrieve: retrieve,
+ validate: validate,
+ }
+
+ return &c
+}
diff --git a/api/rosetta/data.go b/api/rosetta/data.go
new file mode 100755
index 0000000..9124643
--- /dev/null
+++ b/api/rosetta/data.go
@@ -0,0 +1,34 @@
+// Copyright 2021 Optakt Labs OÜ
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+package rosetta
+
+// Data implements the Rosetta Data API specification.
+// See https://www.rosetta-api.org/docs/data_api_introduction.html
+type Data struct {
+ config Configuration
+ retrieve Retriever
+ validate Validator
+}
+
+// NewData creates a new instance of the Data API using the given configuration to answer configuration queries
+// and the given retriever to answer blockchain data queries.
+func NewData(config Configuration, retrieve Retriever, validate Validator) *Data {
+ d := Data{
+ config: config,
+ retrieve: retrieve,
+ validate: validate,
+ }
+ return &d
+}
diff --git a/api/rosetta/data_integration_test.go b/api/rosetta/data_integration_test.go
new file mode 100755
index 0000000..9bcbd0c
--- /dev/null
+++ b/api/rosetta/data_integration_test.go
@@ -0,0 +1,366 @@
+// Copyright 2021 Optakt Labs OÜ
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+//go:build integration
+// +build integration
+
+package rosetta_test
+
+import (
+ "bytes"
+ "encoding/hex"
+ "encoding/json"
+ "fmt"
+ "net/http"
+ "net/http/httptest"
+ "runtime"
+ "strings"
+ "testing"
+ "time"
+
+ "github.com/dgraph-io/badger/v2"
+ "github.com/klauspost/compress/zstd"
+ "github.com/labstack/echo/v4"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+
+ "github.com/onflow/flow-go/model/flow"
+
+ "github.com/optakt/flow-dps/codec/zbor"
+ "github.com/optakt/flow-dps/models/dps"
+ "github.com/optakt/flow-dps/service/index"
+ "github.com/optakt/flow-dps/service/invoker"
+ "github.com/optakt/flow-dps/service/storage"
+ "github.com/optakt/flow-rosetta/api/rosetta"
+ "github.com/optakt/flow-rosetta/rosetta/configuration"
+ "github.com/optakt/flow-rosetta/rosetta/converter"
+ "github.com/optakt/flow-rosetta/rosetta/identifier"
+ "github.com/optakt/flow-rosetta/rosetta/meta"
+ "github.com/optakt/flow-rosetta/rosetta/retriever"
+ "github.com/optakt/flow-rosetta/rosetta/scripts"
+ "github.com/optakt/flow-rosetta/rosetta/validator"
+ "github.com/optakt/flow-rosetta/testing/snapshots"
+)
+
+const (
+ balanceEndpoint = "/account/balance"
+ blockEndpoint = "/block"
+ transactionEndpoint = "/block/transaction"
+ listEndpoint = "/network/list"
+ optionsEndpoint = "/network/options"
+ statusEndpoint = "/network/status"
+
+ invalidBlockchain = "invalid-blockchain"
+ invalidNetwork = "invalid-network"
+ invalidToken = "invalid-token"
+
+ invalidBlockHash = "af528bb047d6cd1400a326bb127d689607a096f5ccd81d8903dfebbac26afb2z" // invalid hex value
+)
+
+func setupDB(t *testing.T) *badger.DB {
+ t.Helper()
+
+ opts := badger.DefaultOptions("").
+ WithInMemory(true).
+ WithLogger(nil)
+
+ db, err := badger.Open(opts)
+ require.NoError(t, err)
+
+ reader := hex.NewDecoder(strings.NewReader(snapshots.Rosetta))
+
+ decompressor, err := zstd.NewReader(reader,
+ zstd.WithDecoderDicts(zbor.Dictionary),
+ )
+ require.NoError(t, err)
+
+ err = db.Load(decompressor, runtime.GOMAXPROCS(0))
+ require.NoError(t, err)
+
+ return db
+}
+
+func setupAPI(t *testing.T, db *badger.DB) *rosetta.Data {
+ t.Helper()
+
+ rosetta.EnableSmartCodes()
+
+ codec := zbor.NewCodec()
+ storage := storage.New(codec)
+ index := index.NewReader(db, storage)
+
+ params := dps.FlowParams[dps.FlowTestnet]
+ config := configuration.New(params.ChainID)
+ validate := validator.New(params, index, config)
+ generate := scripts.NewGenerator(params)
+ invoke, err := invoker.New(index)
+ require.NoError(t, err)
+ convert, err := converter.New(generate)
+ require.NoError(t, err)
+ retrieve := retriever.New(params, index, validate, generate, invoke, convert)
+ controller := rosetta.NewData(config, retrieve, validate)
+
+ return controller
+}
+
+func setupRecorder(endpoint string, input interface{}, options ...func(*http.Request)) (*httptest.ResponseRecorder, echo.Context, error) {
+
+ payload, ok := input.([]byte)
+ if !ok {
+ var err error
+ payload, err = json.Marshal(input)
+ if err != nil {
+ return nil, echo.New().AcquireContext(), fmt.Errorf("could not encode input: %w", err)
+ }
+ }
+
+ req := httptest.NewRequest(http.MethodPost, endpoint, bytes.NewReader(payload))
+ req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON)
+
+ for _, opt := range options {
+ opt(req)
+ }
+
+ rec := httptest.NewRecorder()
+
+ ctx := echo.New().NewContext(req, rec)
+
+ return rec, ctx, nil
+}
+
+func checkRosettaError(statusCode int, def meta.ErrorDefinition) func(t assert.TestingT, err error, v ...interface{}) bool {
+
+ return func(t assert.TestingT, err error, v ...interface{}) bool {
+ // return false if any of the asserts failed
+ success := true
+ success = success && assert.Error(t, err)
+
+ if !assert.IsType(t, &echo.HTTPError{}, err) {
+ return false
+ }
+ echoErr := err.(*echo.HTTPError)
+
+ success = success && assert.Equal(t, statusCode, echoErr.Code)
+
+ if !assert.IsType(t, rosetta.Error{}, echoErr.Message) {
+ return false
+ }
+
+ gotErr := echoErr.Message.(rosetta.Error)
+
+ success = success && assert.Equal(t, def, gotErr.ErrorDefinition)
+ return success
+ }
+}
+
+// defaultNetwork returns the Network identifier common for all requests.
+func defaultNetwork() identifier.Network {
+ return identifier.Network{
+ Blockchain: dps.FlowBlockchain,
+ Network: dps.FlowTestnet.String(),
+ }
+}
+
+// defaultCurrency returns the Currency spec common for all requests.
+// For now this only gets the FLOW tokens.
+func defaultCurrency() []identifier.Currency {
+ return []identifier.Currency{
+ {
+ Symbol: dps.FlowSymbol,
+ Decimals: dps.FlowDecimals,
+ },
+ }
+}
+
+func validateBlock(t *testing.T, height uint64, hash string) validateBlockFunc {
+ t.Helper()
+
+ return func(rosBlockID identifier.Block) {
+ assert.Equal(t, hash, rosBlockID.Hash)
+ require.NotNil(t, rosBlockID.Index)
+ assert.Equal(t, height, *rosBlockID.Index)
+ }
+}
+
+func validateByHeader(t *testing.T, header flow.Header) validateBlockFunc {
+ return validateBlock(t, header.Height, header.ID().String())
+}
+
+func getUint64P(n uint64) *uint64 {
+ return &n
+}
+
+func knownHeader(height uint64) flow.Header {
+
+ switch height {
+
+ case 0:
+ return flow.Header{
+ ChainID: dps.FlowTestnet,
+ ParentID: flow.Identifier{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0},
+ Height: 0,
+ PayloadHash: flow.Identifier{0x7b, 0x3b, 0x31, 0x3b, 0xd8, 0x3e, 0x1, 0xd1, 0x3c, 0x44, 0x9d, 0x4d, 0xd4, 0xba, 0xc0, 0x41, 0x37, 0xf5, 0x9, 0xb, 0xcb, 0x30, 0x5d, 0xdd, 0x75, 0x2, 0x98, 0xbd, 0x16, 0xe5, 0x33, 0x9b},
+ Timestamp: time.Unix(0, 1621337233243086400).UTC(),
+ View: 0,
+ ParentVoterIDs: []flow.Identifier{},
+ ProposerID: flow.Identifier{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0},
+ }
+
+ case 1:
+ return flow.Header{
+ ChainID: dps.FlowTestnet,
+ ParentID: flow.Identifier{0xd4, 0x7b, 0x1b, 0xf7, 0xf3, 0x7e, 0x19, 0x2c, 0xf8, 0x3d, 0x2b, 0xee, 0x3f, 0x63, 0x32, 0xb0, 0xd9, 0xb1, 0x5c, 0xa, 0xa7, 0x66, 0xd, 0x1e, 0x53, 0x22, 0xea, 0x96, 0x46, 0x67, 0xb3, 0x33},
+ Height: 1,
+ PayloadHash: flow.Identifier{0x7b, 0x3b, 0x31, 0x3b, 0xd8, 0x3e, 0x1, 0xd1, 0x3c, 0x44, 0x9d, 0x4d, 0xd4, 0xba, 0xc0, 0x41, 0x37, 0xf5, 0x9, 0xb, 0xcb, 0x30, 0x5d, 0xdd, 0x75, 0x2, 0x98, 0xbd, 0x16, 0xe5, 0x33, 0x9b},
+ Timestamp: time.Unix(0, 1621337323243086400).UTC(),
+ View: 2,
+ ParentVoterIDs: []flow.Identifier{
+ {0x5, 0x55, 0x33, 0x7e, 0xf, 0x66, 0x1e, 0xc9, 0xb0, 0x7e, 0xbb, 0x69, 0x46, 0x8, 0x13, 0x16, 0xfa, 0x65, 0xc0, 0xba, 0xca, 0x6b, 0xd4, 0x70, 0x5b, 0xf6, 0x9d, 0x56, 0xa9, 0xf5, 0xb8, 0xa3},
+ {0x45, 0x51, 0xbe, 0x34, 0xd9, 0xf7, 0xa9, 0x3b, 0x0, 0xd2, 0x87, 0xbd, 0x68, 0x3f, 0x7d, 0xd6, 0x34, 0x5e, 0x65, 0x90, 0x72, 0x40, 0x40, 0x5, 0x54, 0xfb, 0xdf, 0xa1, 0x69, 0x7d, 0x3b, 0xfa},
+ {0xd3, 0x5f, 0xac, 0xa6, 0x7a, 0xbc, 0x6, 0xc3, 0x34, 0xb1, 0xe5, 0xa7, 0x88, 0x23, 0x98, 0xda, 0xe9, 0xc1, 0xda, 0xd9, 0x13, 0xe5, 0x60, 0x9e, 0xe1, 0xd4, 0x63, 0xd5, 0x5a, 0x22, 0x44, 0xf7},
+ },
+ ParentVoterSigData: []byte{0xa0, 0x2a, 0xed, 0xa7, 0xc4, 0xe1, 0x40, 0x8e, 0x70, 0xe7, 0xa6, 0x7d, 0x81, 0x99, 0x24, 0xf4, 0x7c, 0x30, 0x42, 0x2, 0xe5, 0xaa, 0xfa, 0x89, 0x89, 0xda, 0x9d, 0x22, 0xb5, 0x45, 0xb0, 0xc2, 0xa4, 0x4c, 0x4b, 0xf3, 0xe1, 0xdf, 0x31, 0x73, 0xa2, 0x3e, 0x48, 0x5, 0xb4, 0xec, 0x5d, 0xcf, 0x8a, 0x6f, 0x42, 0xe7, 0xdd, 0xad, 0x7d, 0x4b, 0x7e, 0xc, 0xcb, 0xc, 0x6, 0x64, 0x10, 0x86, 0x4d, 0xd6, 0x89, 0x3a, 0x6f, 0x1e, 0xc4, 0xef, 0x6c, 0x18, 0xbf, 0xd5, 0x3a, 0x36, 0x25, 0xf0, 0xf9, 0xb0, 0x1f, 0x27, 0x4e, 0x4d, 0x72, 0x34, 0xf4, 0x51, 0xcc, 0x7d, 0x81, 0x86, 0xed, 0xb2},
+ ProposerID: flow.Identifier{0x5, 0x55, 0x33, 0x7e, 0xf, 0x66, 0x1e, 0xc9, 0xb0, 0x7e, 0xbb, 0x69, 0x46, 0x8, 0x13, 0x16, 0xfa, 0x65, 0xc0, 0xba, 0xca, 0x6b, 0xd4, 0x70, 0x5b, 0xf6, 0x9d, 0x56, 0xa9, 0xf5, 0xb8, 0xa3},
+ ProposerSigData: []byte{0x8f, 0x7a, 0x5c, 0xbb, 0x4d, 0xfa, 0x46, 0x6, 0xe9, 0x9a, 0xd6, 0xea, 0xa1, 0xa3, 0x1a, 0x3b, 0xb4, 0xa4, 0xc0, 0xa4, 0x4a, 0xa6, 0xe7, 0xe6, 0x8b, 0x5e, 0x5e, 0x8b, 0x3f, 0xf, 0xa3, 0x32, 0x68, 0xee, 0x59, 0x20, 0x97, 0xa2, 0x38, 0xd5, 0x25, 0x64, 0xc3, 0x54, 0x1f, 0x1f, 0x9c, 0x5a, 0xaa, 0xc5, 0x1, 0xf3, 0xff, 0x3f, 0x83, 0x4, 0x9f, 0xed, 0xc3, 0x84, 0xf8, 0x5, 0xe6, 0x15, 0xf, 0x21, 0x50, 0x27, 0x3a, 0x72, 0xe3, 0xa0, 0x35, 0xec, 0x43, 0x48, 0x40, 0x9b, 0xef, 0xfa, 0x1b, 0x20, 0xb6, 0x7, 0x53, 0xcf, 0x38, 0x9f, 0x87, 0xf0, 0x52, 0x47, 0xfc, 0xc4, 0x70, 0x47},
+ }
+
+ case 13:
+ return flow.Header{
+ ChainID: dps.FlowTestnet,
+ ParentID: flow.Identifier{0x90, 0x35, 0xc5, 0x58, 0x37, 0x9b, 0x20, 0x8e, 0xba, 0x11, 0x13, 0xc, 0x92, 0x85, 0x37, 0xfe, 0x50, 0xad, 0x93, 0xcd, 0xee, 0x31, 0x49, 0x80, 0xfc, 0xcb, 0x69, 0x5a, 0xa3, 0x1d, 0xf7, 0xfc},
+ Height: 13,
+ PayloadHash: flow.Identifier{0xd0, 0x43, 0x96, 0xd9, 0x5, 0x79, 0xea, 0xe8, 0xfc, 0xf5, 0x90, 0x6f, 0xce, 0xde, 0x4d, 0x26, 0xbe, 0x66, 0x7d, 0x5d, 0x6e, 0x36, 0x8c, 0xd1, 0x99, 0xed, 0x8a, 0x66, 0x17, 0x84, 0x67, 0xf2},
+ Timestamp: time.Unix(0, 1621338403243086400).UTC(),
+ View: 14,
+ ParentVoterIDs: []flow.Identifier{
+ {0x5, 0x55, 0x33, 0x7e, 0xf, 0x66, 0x1e, 0xc9, 0xb0, 0x7e, 0xbb, 0x69, 0x46, 0x8, 0x13, 0x16, 0xfa, 0x65, 0xc0, 0xba, 0xca, 0x6b, 0xd4, 0x70, 0x5b, 0xf6, 0x9d, 0x56, 0xa9, 0xf5, 0xb8, 0xa3},
+ {0x45, 0x51, 0xbe, 0x34, 0xd9, 0xf7, 0xa9, 0x3b, 0x0, 0xd2, 0x87, 0xbd, 0x68, 0x3f, 0x7d, 0xd6, 0x34, 0x5e, 0x65, 0x90, 0x72, 0x40, 0x40, 0x5, 0x54, 0xfb, 0xdf, 0xa1, 0x69, 0x7d, 0x3b, 0xfa},
+ {0xd3, 0x5f, 0xac, 0xa6, 0x7a, 0xbc, 0x6, 0xc3, 0x34, 0xb1, 0xe5, 0xa7, 0x88, 0x23, 0x98, 0xda, 0xe9, 0xc1, 0xda, 0xd9, 0x13, 0xe5, 0x60, 0x9e, 0xe1, 0xd4, 0x63, 0xd5, 0x5a, 0x22, 0x44, 0xf7},
+ },
+ ParentVoterSigData: []byte{0x92, 0xf6, 0x89, 0x55, 0x76, 0xb, 0x5, 0xe5, 0x89, 0xae, 0x3e, 0x21, 0xa6, 0x4a, 0x4f, 0xb6, 0xd4, 0x40, 0xcc, 0x94, 0x90, 0x8f, 0x40, 0xeb, 0xcd, 0xfd, 0x30, 0x45, 0xd7, 0x94, 0xc8, 0x95, 0xfe, 0xf1, 0x7e, 0xd8, 0x71, 0xce, 0x6c, 0x3, 0xb8, 0x4f, 0x5f, 0x8, 0x30, 0x2, 0x8a, 0x85, 0x90, 0x2a, 0xc5, 0xd, 0x81, 0x49, 0x11, 0xd9, 0x37, 0x35, 0x6f, 0xf9, 0x3f, 0x7b, 0x52, 0x4, 0xdb, 0x5a, 0x36, 0x81, 0xda, 0xa6, 0x47, 0xb5, 0xd9, 0xa7, 0xec, 0x6, 0xda, 0x34, 0x70, 0xdf, 0x8, 0x4a, 0xd5, 0xd0, 0x14, 0xf7, 0x2d, 0xd7, 0x5b, 0x66, 0x39, 0x64, 0x3c, 0xf1, 0xbb, 0xe4},
+ ProposerID: flow.Identifier{0x5, 0x55, 0x33, 0x7e, 0xf, 0x66, 0x1e, 0xc9, 0xb0, 0x7e, 0xbb, 0x69, 0x46, 0x8, 0x13, 0x16, 0xfa, 0x65, 0xc0, 0xba, 0xca, 0x6b, 0xd4, 0x70, 0x5b, 0xf6, 0x9d, 0x56, 0xa9, 0xf5, 0xb8, 0xa3},
+ ProposerSigData: []byte{0x98, 0xe2, 0xf9, 0x46, 0xc4, 0xd7, 0x71, 0xc6, 0xf6, 0x56, 0x21, 0xe, 0xe7, 0xa4, 0x9c, 0xaa, 0xc, 0x3f, 0x7a, 0x75, 0xb9, 0x53, 0x95, 0x37, 0xdd, 0xb7, 0x4b, 0x7f, 0xfc, 0x1e, 0x1a, 0xe9, 0xfe, 0xbb, 0x56, 0x2e, 0xb8, 0x6e, 0xf6, 0xd8, 0x25, 0x4d, 0x5f, 0xee, 0x46, 0x1d, 0xd, 0xd4, 0x82, 0xeb, 0x7, 0xde, 0xa2, 0x8, 0x58, 0x13, 0xba, 0xfb, 0xeb, 0x2e, 0xcd, 0x88, 0x2e, 0x7c, 0x1b, 0xe8, 0xc5, 0x1d, 0x84, 0xe, 0xa2, 0x10, 0xbe, 0xe3, 0xb6, 0x26, 0x87, 0x4b, 0x6c, 0xbf, 0xc2, 0xe0, 0x85, 0xf4, 0x7e, 0xf5, 0xf3, 0x55, 0x5d, 0xd3, 0x49, 0xff, 0xc8, 0xe3, 0xb5, 0x53},
+ }
+
+ case 43:
+ return flow.Header{
+ ChainID: dps.FlowTestnet,
+ ParentID: flow.Identifier{0x91, 0xc0, 0xb, 0x22, 0xdc, 0x9b, 0x84, 0x28, 0x1d, 0x29, 0x3f, 0x6e, 0x1f, 0xf6, 0x80, 0x13, 0x32, 0x39, 0xad, 0xdd, 0x8b, 0x2, 0x20, 0xa2, 0x44, 0x55, 0x4e, 0x1d, 0x96, 0xae, 0xd8, 0xe0},
+ Height: 43,
+ PayloadHash: flow.Identifier{0x5, 0xae, 0xad, 0x1e, 0xaa, 0x5f, 0x40, 0x85, 0xf0, 0xb4, 0xa2, 0x67, 0x67, 0x3d, 0x13, 0xc4, 0x6, 0x26, 0xbf, 0xe9, 0x3d, 0xf9, 0x90, 0x38, 0x5c, 0xf3, 0xbc, 0x7a, 0xfd, 0x77, 0x15, 0x21},
+ Timestamp: time.Unix(0, 1621341103243086400).UTC(),
+ View: 44,
+ ParentVoterIDs: []flow.Identifier{
+ {0x5, 0x55, 0x33, 0x7e, 0xf, 0x66, 0x1e, 0xc9, 0xb0, 0x7e, 0xbb, 0x69, 0x46, 0x8, 0x13, 0x16, 0xfa, 0x65, 0xc0, 0xba, 0xca, 0x6b, 0xd4, 0x70, 0x5b, 0xf6, 0x9d, 0x56, 0xa9, 0xf5, 0xb8, 0xa3},
+ {0x45, 0x51, 0xbe, 0x34, 0xd9, 0xf7, 0xa9, 0x3b, 0x0, 0xd2, 0x87, 0xbd, 0x68, 0x3f, 0x7d, 0xd6, 0x34, 0x5e, 0x65, 0x90, 0x72, 0x40, 0x40, 0x5, 0x54, 0xfb, 0xdf, 0xa1, 0x69, 0x7d, 0x3b, 0xfa},
+ {0xd3, 0x5f, 0xac, 0xa6, 0x7a, 0xbc, 0x6, 0xc3, 0x34, 0xb1, 0xe5, 0xa7, 0x88, 0x23, 0x98, 0xda, 0xe9, 0xc1, 0xda, 0xd9, 0x13, 0xe5, 0x60, 0x9e, 0xe1, 0xd4, 0x63, 0xd5, 0x5a, 0x22, 0x44, 0xf7},
+ },
+ ParentVoterSigData: []byte{0x95, 0x8e, 0x2b, 0x47, 0x8, 0xfa, 0x12, 0xa5, 0x3, 0x99, 0xbc, 0xce, 0xb5, 0x82, 0xac, 0x71, 0x7a, 0x9, 0x87, 0x60, 0x17, 0x70, 0x1c, 0x51, 0xa, 0xef, 0x45, 0x9e, 0x7, 0xc1, 0x4, 0x92, 0xa, 0x7b, 0xd6, 0x13, 0xb0, 0x6c, 0x45, 0x4c, 0x2c, 0xba, 0xc4, 0xa3, 0xb0, 0xf6, 0x87, 0x64, 0x93, 0x83, 0xca, 0x2b, 0x48, 0x41, 0x7f, 0x84, 0x2b, 0xf1, 0x84, 0xda, 0x2e, 0xec, 0xd7, 0xc, 0xb6, 0x54, 0x19, 0x8e, 0x20, 0x1e, 0xa8, 0x8c, 0xb0, 0x38, 0xab, 0xc4, 0x40, 0x13, 0x0, 0xfc, 0x55, 0x26, 0xb8, 0xc5, 0x5a, 0xa9, 0xd4, 0xe4, 0x9f, 0xf7, 0x3c, 0x68, 0x68, 0xdf, 0x38, 0x54},
+ ProposerID: flow.Identifier{0x5, 0x55, 0x33, 0x7e, 0xf, 0x66, 0x1e, 0xc9, 0xb0, 0x7e, 0xbb, 0x69, 0x46, 0x8, 0x13, 0x16, 0xfa, 0x65, 0xc0, 0xba, 0xca, 0x6b, 0xd4, 0x70, 0x5b, 0xf6, 0x9d, 0x56, 0xa9, 0xf5, 0xb8, 0xa3},
+ ProposerSigData: []byte{0xb0, 0x2, 0x4f, 0xfc, 0x71, 0xba, 0x38, 0xc1, 0x24, 0x79, 0xa0, 0xd2, 0x66, 0xe3, 0xfb, 0x20, 0xe2, 0x2e, 0x27, 0xe4, 0x99, 0x91, 0xc4, 0x44, 0x28, 0x85, 0x87, 0x4, 0x44, 0x54, 0xcb, 0x47, 0x28, 0xd5, 0x8f, 0xc4, 0x89, 0x38, 0xfb, 0xce, 0xf6, 0xba, 0x35, 0x74, 0xf1, 0x52, 0xb4, 0x5a, 0x8e, 0x4e, 0x4a, 0xc3, 0x23, 0xe3, 0xfe, 0xac, 0x9, 0xf9, 0x1, 0x37, 0xd4, 0xa, 0x54, 0x81, 0x63, 0x93, 0x56, 0xeb, 0x7d, 0x23, 0x13, 0x20, 0x7f, 0xb7, 0x47, 0xe3, 0x33, 0xb8, 0x2e, 0x5, 0xc3, 0x96, 0xdd, 0x20, 0x56, 0xca, 0x48, 0xd1, 0x6b, 0x48, 0x10, 0x6, 0x26, 0xb3, 0x84, 0x18},
+ }
+
+ case 44:
+ return flow.Header{
+ ChainID: dps.FlowTestnet,
+ ParentID: flow.Identifier{0xda, 0xb1, 0x86, 0xb4, 0x51, 0x99, 0xc0, 0xc2, 0x60, 0x60, 0xea, 0x9, 0x28, 0x8b, 0x2f, 0x16, 0x3, 0x2d, 0xa4, 0xf, 0xc5, 0x4c, 0x81, 0xbb, 0x2a, 0x82, 0x67, 0xa5, 0xc1, 0x39, 0x6, 0xe6},
+ Height: 44,
+ PayloadHash: flow.Identifier{0x80, 0xfe, 0xaf, 0x28, 0x4f, 0x8a, 0x51, 0x6c, 0x8c, 0x8, 0x6a, 0x9f, 0xae, 0xc0, 0xbd, 0xbb, 0x6b, 0xcd, 0xf1, 0xc8, 0x2b, 0x4f, 0xc6, 0xdb, 0x35, 0xff, 0x75, 0x42, 0x11, 0x8, 0x1b, 0xd9},
+ Timestamp: time.Unix(0, 1621341193243086400).UTC(),
+ View: 45,
+ ParentVoterIDs: []flow.Identifier{
+ {0x45, 0x51, 0xbe, 0x34, 0xd9, 0xf7, 0xa9, 0x3b, 0x0, 0xd2, 0x87, 0xbd, 0x68, 0x3f, 0x7d, 0xd6, 0x34, 0x5e, 0x65, 0x90, 0x72, 0x40, 0x40, 0x5, 0x54, 0xfb, 0xdf, 0xa1, 0x69, 0x7d, 0x3b, 0xfa},
+ {0x5, 0x55, 0x33, 0x7e, 0xf, 0x66, 0x1e, 0xc9, 0xb0, 0x7e, 0xbb, 0x69, 0x46, 0x8, 0x13, 0x16, 0xfa, 0x65, 0xc0, 0xba, 0xca, 0x6b, 0xd4, 0x70, 0x5b, 0xf6, 0x9d, 0x56, 0xa9, 0xf5, 0xb8, 0xa3},
+ {0xd3, 0x5f, 0xac, 0xa6, 0x7a, 0xbc, 0x6, 0xc3, 0x34, 0xb1, 0xe5, 0xa7, 0x88, 0x23, 0x98, 0xda, 0xe9, 0xc1, 0xda, 0xd9, 0x13, 0xe5, 0x60, 0x9e, 0xe1, 0xd4, 0x63, 0xd5, 0x5a, 0x22, 0x44, 0xf7},
+ },
+ ParentVoterSigData: []byte{0x8f, 0x90, 0xd9, 0xf6, 0x9, 0xc9, 0x9, 0xb7, 0x5b, 0x46, 0x7d, 0x4a, 0x17, 0xdb, 0x4e, 0xb7, 0xce, 0xc0, 0x8, 0x7e, 0xcb, 0xf6, 0xde, 0x76, 0xc6, 0xf5, 0x31, 0xbe, 0x13, 0xa3, 0x90, 0xc, 0xc3, 0x8f, 0x33, 0xeb, 0x50, 0xfc, 0x4d, 0x93, 0xb3, 0x64, 0xe8, 0x80, 0x74, 0x51, 0xc4, 0xb3, 0x8c, 0x41, 0xd7, 0xb5, 0xd5, 0x51, 0x72, 0x15, 0x4f, 0x7, 0x95, 0xfb, 0xcf, 0x54, 0xa7, 0x92, 0xa0, 0x90, 0x98, 0x9d, 0x57, 0xc2, 0xb2, 0x7a, 0xe3, 0x1e, 0x5b, 0xbd, 0x2c, 0x5d, 0x71, 0x23, 0x48, 0x87, 0xda, 0xb7, 0x4a, 0x13, 0xf1, 0xea, 0x37, 0x41, 0x53, 0xb7, 0xf4, 0x1f, 0x53, 0x30},
+ ProposerID: flow.Identifier{0x45, 0x51, 0xbe, 0x34, 0xd9, 0xf7, 0xa9, 0x3b, 0x0, 0xd2, 0x87, 0xbd, 0x68, 0x3f, 0x7d, 0xd6, 0x34, 0x5e, 0x65, 0x90, 0x72, 0x40, 0x40, 0x5, 0x54, 0xfb, 0xdf, 0xa1, 0x69, 0x7d, 0x3b, 0xfa},
+ ProposerSigData: []byte{0x8f, 0x30, 0x2d, 0x1f, 0xb1, 0x6c, 0x30, 0x24, 0xf0, 0x6, 0x76, 0x95, 0x30, 0xeb, 0xda, 0x22, 0xea, 0x7f, 0x4, 0x8a, 0x2e, 0x76, 0x8a, 0x72, 0xcd, 0x91, 0x29, 0x9b, 0xca, 0x3e, 0xf, 0x78, 0x31, 0xf, 0x79, 0x1, 0x68, 0xb4, 0x26, 0xc1, 0x92, 0x48, 0xf8, 0xaa, 0xb6, 0x41, 0x85, 0x70, 0xb3, 0x3, 0x23, 0x4e, 0x22, 0xf0, 0x1a, 0x69, 0x73, 0x55, 0x4c, 0x91, 0xdb, 0xde, 0x8b, 0x7f, 0xf6, 0xa8, 0xe1, 0x6f, 0xf4, 0xf7, 0xd3, 0x51, 0xd7, 0xd2, 0xf5, 0x90, 0x1e, 0x2a, 0x95, 0xa, 0xd5, 0x11, 0xf3, 0xec, 0x53, 0x87, 0x5, 0xf, 0x21, 0xba, 0xfe, 0x98, 0x97, 0x93, 0xb3, 0xc},
+ }
+
+ case 50:
+ return flow.Header{
+ ChainID: dps.FlowTestnet,
+ ParentID: flow.Identifier{0x4, 0x33, 0xf7, 0x22, 0x3, 0x78, 0x7a, 0x45, 0x20, 0x81, 0xef, 0x63, 0xf4, 0xfb, 0x8b, 0x33, 0xac, 0xc7, 0x3d, 0x46, 0xa5, 0x8b, 0x73, 0xca, 0x2, 0xaa, 0x65, 0x28, 0xd0, 0x57, 0x87, 0xd6},
+ Height: 50,
+ PayloadHash: flow.Identifier{0x8b, 0x81, 0x99, 0xeb, 0xac, 0xdc, 0x6, 0x5b, 0xbc, 0x74, 0x65, 0xc, 0xc5, 0x67, 0xc5, 0x3a, 0x53, 0x26, 0xf8, 0x47, 0xea, 0x3b, 0x97, 0xcd, 0xb9, 0x42, 0x40, 0x8f, 0x45, 0x43, 0x10, 0xd6},
+ Timestamp: time.Unix(0, 1621341733243086400).UTC(),
+ View: 51,
+ ParentVoterIDs: []flow.Identifier{
+ {0xd3, 0x5f, 0xac, 0xa6, 0x7a, 0xbc, 0x6, 0xc3, 0x34, 0xb1, 0xe5, 0xa7, 0x88, 0x23, 0x98, 0xda, 0xe9, 0xc1, 0xda, 0xd9, 0x13, 0xe5, 0x60, 0x9e, 0xe1, 0xd4, 0x63, 0xd5, 0x5a, 0x22, 0x44, 0xf7},
+ {0x5, 0x55, 0x33, 0x7e, 0xf, 0x66, 0x1e, 0xc9, 0xb0, 0x7e, 0xbb, 0x69, 0x46, 0x8, 0x13, 0x16, 0xfa, 0x65, 0xc0, 0xba, 0xca, 0x6b, 0xd4, 0x70, 0x5b, 0xf6, 0x9d, 0x56, 0xa9, 0xf5, 0xb8, 0xa3},
+ {0x45, 0x51, 0xbe, 0x34, 0xd9, 0xf7, 0xa9, 0x3b, 0x0, 0xd2, 0x87, 0xbd, 0x68, 0x3f, 0x7d, 0xd6, 0x34, 0x5e, 0x65, 0x90, 0x72, 0x40, 0x40, 0x5, 0x54, 0xfb, 0xdf, 0xa1, 0x69, 0x7d, 0x3b, 0xfa},
+ },
+ ParentVoterSigData: []byte{0x98, 0x14, 0xd7, 0xc6, 0x82, 0x79, 0x8d, 0x5f, 0x59, 0x21, 0x20, 0xb9, 0xc3, 0x10, 0xb4, 0xed, 0x48, 0xaf, 0xd1, 0x47, 0xee, 0xb2, 0xd8, 0x55, 0xc9, 0x4f, 0x23, 0xe2, 0x33, 0x28, 0xc0, 0xef, 0xd5, 0x85, 0x6b, 0xab, 0x38, 0x2c, 0xab, 0xcd, 0x1e, 0x68, 0x4, 0xa2, 0x96, 0xc3, 0x1f, 0x72, 0x91, 0x5f, 0xd6, 0x94, 0x9d, 0x29, 0xa8, 0x6c, 0xfc, 0xc1, 0xe, 0x5, 0x2f, 0x2d, 0xf6, 0xa0, 0x75, 0x50, 0x83, 0xb, 0x51, 0xa9, 0x8, 0x81, 0xa1, 0xc0, 0xa2, 0xa2, 0x42, 0x2d, 0xa6, 0x5c, 0xd, 0x60, 0x12, 0x61, 0x74, 0xf1, 0xb8, 0x9e, 0x16, 0x45, 0x25, 0xaa, 0xe8, 0xe4, 0x60, 0x5a},
+ ProposerID: flow.Identifier{0xd3, 0x5f, 0xac, 0xa6, 0x7a, 0xbc, 0x6, 0xc3, 0x34, 0xb1, 0xe5, 0xa7, 0x88, 0x23, 0x98, 0xda, 0xe9, 0xc1, 0xda, 0xd9, 0x13, 0xe5, 0x60, 0x9e, 0xe1, 0xd4, 0x63, 0xd5, 0x5a, 0x22, 0x44, 0xf7},
+ ProposerSigData: []byte{0xa0, 0x23, 0x8e, 0x9a, 0x10, 0xf9, 0xe1, 0x61, 0xe9, 0xa4, 0xf0, 0xaf, 0xa6, 0x97, 0x12, 0x15, 0xd5, 0xd8, 0x39, 0x13, 0xc1, 0x3f, 0x89, 0x38, 0x7d, 0x9, 0x86, 0x57, 0x61, 0xe4, 0x5e, 0x9, 0xa6, 0x8, 0x54, 0x87, 0x9a, 0xe3, 0x14, 0xf1, 0xf6, 0x71, 0xee, 0xc1, 0x46, 0x40, 0x3c, 0x9f, 0x80, 0x5c, 0xe7, 0x9a, 0x1e, 0x5a, 0x4e, 0xfa, 0x27, 0xdf, 0x0, 0x5, 0x58, 0x54, 0xf0, 0x5f, 0x12, 0x73, 0x9d, 0x2f, 0xb8, 0x64, 0x2c, 0x13, 0x31, 0x5a, 0x38, 0x4, 0xfe, 0x13, 0xe, 0xea, 0x5b, 0xb0, 0x31, 0x42, 0xfb, 0x4e, 0xfe, 0x44, 0x8a, 0xb0, 0x15, 0x1a, 0x26, 0x21, 0x31, 0xfc}}
+
+ case 165:
+ return flow.Header{
+ ChainID: dps.FlowTestnet,
+ ParentID: flow.Identifier{0x99, 0xe4, 0x79, 0x7, 0x96, 0x13, 0x34, 0x81, 0x82, 0x9c, 0xe, 0xd6, 0x43, 0xcb, 0x21, 0x87, 0xa8, 0xab, 0x3a, 0xbf, 0x23, 0xa1, 0x38, 0x5d, 0xe7, 0xa8, 0xf8, 0x64, 0x40, 0x35, 0xc8, 0x82},
+ Height: 165,
+ PayloadHash: flow.Identifier{0x96, 0x97, 0xac, 0xda, 0xba, 0x28, 0xfb, 0xe9, 0x75, 0xeb, 0xc, 0x56, 0x21, 0x94, 0x1b, 0x54, 0x7e, 0x71, 0x58, 0x26, 0xbd, 0xa6, 0xa8, 0xce, 0xd6, 0xcf, 0x7d, 0xa9, 0x66, 0xec, 0x37, 0xb7},
+ Timestamp: time.Unix(0, 1621352083243086400).UTC(),
+ View: 166,
+ ParentVoterIDs: []flow.Identifier{
+ {0x5, 0x55, 0x33, 0x7e, 0xf, 0x66, 0x1e, 0xc9, 0xb0, 0x7e, 0xbb, 0x69, 0x46, 0x8, 0x13, 0x16, 0xfa, 0x65, 0xc0, 0xba, 0xca, 0x6b, 0xd4, 0x70, 0x5b, 0xf6, 0x9d, 0x56, 0xa9, 0xf5, 0xb8, 0xa3},
+ {0x45, 0x51, 0xbe, 0x34, 0xd9, 0xf7, 0xa9, 0x3b, 0x0, 0xd2, 0x87, 0xbd, 0x68, 0x3f, 0x7d, 0xd6, 0x34, 0x5e, 0x65, 0x90, 0x72, 0x40, 0x40, 0x5, 0x54, 0xfb, 0xdf, 0xa1, 0x69, 0x7d, 0x3b, 0xfa},
+ {0xd3, 0x5f, 0xac, 0xa6, 0x7a, 0xbc, 0x6, 0xc3, 0x34, 0xb1, 0xe5, 0xa7, 0x88, 0x23, 0x98, 0xda, 0xe9, 0xc1, 0xda, 0xd9, 0x13, 0xe5, 0x60, 0x9e, 0xe1, 0xd4, 0x63, 0xd5, 0x5a, 0x22, 0x44, 0xf7},
+ },
+ ParentVoterSigData: []byte{0xb2, 0x9f, 0x53, 0x34, 0x5c, 0x98, 0xae, 0xb, 0xc4, 0x2a, 0xa1, 0xc4, 0xc5, 0xeb, 0x14, 0xe9, 0xc3, 0xb9, 0x33, 0xb6, 0x6e, 0xe7, 0xe0, 0x2, 0x51, 0x5b, 0xc5, 0x81, 0xfc, 0xf5, 0xbd, 0x2, 0x61, 0xde, 0xea, 0x87, 0x5a, 0x6, 0xab, 0xd4, 0x1e, 0xf9, 0x28, 0xc3, 0x84, 0x54, 0x2a, 0x77, 0xb5, 0x7a, 0x83, 0xda, 0x6d, 0xb6, 0x34, 0x90, 0x66, 0x8a, 0xa7, 0x93, 0x3a, 0x5, 0xff, 0xa5, 0xcb, 0x88, 0xd8, 0x58, 0x67, 0xd5, 0xdf, 0xe7, 0x8f, 0xea, 0xdc, 0x76, 0xd6, 0x5b, 0xf0, 0x5, 0x53, 0xd3, 0x83, 0x24, 0xb5, 0x4c, 0x94, 0x31, 0x0, 0x93, 0x3c, 0xf0, 0x7, 0x84, 0x5c, 0x72},
+ ProposerID: flow.Identifier{0x45, 0x51, 0xbe, 0x34, 0xd9, 0xf7, 0xa9, 0x3b, 0x0, 0xd2, 0x87, 0xbd, 0x68, 0x3f, 0x7d, 0xd6, 0x34, 0x5e, 0x65, 0x90, 0x72, 0x40, 0x40, 0x5, 0x54, 0xfb, 0xdf, 0xa1, 0x69, 0x7d, 0x3b, 0xfa},
+ ProposerSigData: []byte{0x8e, 0x8b, 0xcb, 0xef, 0x35, 0x80, 0xbf, 0x85, 0xd6, 0xb2, 0xaa, 0xa, 0x5d, 0x42, 0x8c, 0xcf, 0x40, 0x95, 0x9c, 0x2d, 0x5f, 0xc9, 0x22, 0x1e, 0x8f, 0xe0, 0x62, 0x14, 0x33, 0x70, 0xe5, 0x8b, 0xaa, 0xaf, 0x13, 0x92, 0x7f, 0xc0, 0x7a, 0x3c, 0x2e, 0xf6, 0x88, 0x8a, 0x35, 0x46, 0x3c, 0xa0, 0xb9, 0x37, 0x75, 0xbd, 0x24, 0xe2, 0x81, 0xa1, 0x4e, 0x40, 0x98, 0x31, 0xfb, 0xa4, 0x2e, 0x2b, 0x59, 0x6c, 0x5, 0x67, 0xd7, 0x68, 0xb9, 0xc, 0x51, 0x7d, 0x48, 0xd2, 0x27, 0xb8, 0xe0, 0x97, 0x93, 0xf2, 0xf2, 0x93, 0x1e, 0xe7, 0xe6, 0x9d, 0x6b, 0xc0, 0x64, 0xce, 0x62, 0xf5, 0xdd, 0xe8},
+ }
+
+ case 181:
+ return flow.Header{
+ ChainID: dps.FlowTestnet,
+ ParentID: flow.Identifier{0x9e, 0x33, 0xa6, 0x57, 0x8, 0x8f, 0xc6, 0xf1, 0x62, 0xc2, 0x36, 0xee, 0x1d, 0x8f, 0xc4, 0x78, 0x2c, 0x9f, 0xc1, 0xb6, 0x5b, 0xaf, 0x4a, 0x40, 0x6, 0x24, 0xf7, 0x66, 0x36, 0x9c, 0xaa, 0xd3},
+ Height: 181,
+ PayloadHash: flow.Identifier{0xfb, 0x1a, 0xae, 0xc9, 0x22, 0xeb, 0xbe, 0xa0, 0xb2, 0x36, 0x24, 0x57, 0xda, 0xa6, 0xc0, 0xa, 0x4a, 0x34, 0xe2, 0x11, 0xef, 0x8f, 0x8, 0x7a, 0x4f, 0x71, 0x33, 0xfb, 0xe5, 0x35, 0x45, 0x5a},
+ Timestamp: time.Unix(0, 1621353523243086400).UTC(),
+ View: 182,
+ ParentVoterIDs: []flow.Identifier{
+ {0x5, 0x55, 0x33, 0x7e, 0xf, 0x66, 0x1e, 0xc9, 0xb0, 0x7e, 0xbb, 0x69, 0x46, 0x8, 0x13, 0x16, 0xfa, 0x65, 0xc0, 0xba, 0xca, 0x6b, 0xd4, 0x70, 0x5b, 0xf6, 0x9d, 0x56, 0xa9, 0xf5, 0xb8, 0xa3},
+ {0x45, 0x51, 0xbe, 0x34, 0xd9, 0xf7, 0xa9, 0x3b, 0x0, 0xd2, 0x87, 0xbd, 0x68, 0x3f, 0x7d, 0xd6, 0x34, 0x5e, 0x65, 0x90, 0x72, 0x40, 0x40, 0x5, 0x54, 0xfb, 0xdf, 0xa1, 0x69, 0x7d, 0x3b, 0xfa},
+ {0xd3, 0x5f, 0xac, 0xa6, 0x7a, 0xbc, 0x6, 0xc3, 0x34, 0xb1, 0xe5, 0xa7, 0x88, 0x23, 0x98, 0xda, 0xe9, 0xc1, 0xda, 0xd9, 0x13, 0xe5, 0x60, 0x9e, 0xe1, 0xd4, 0x63, 0xd5, 0x5a, 0x22, 0x44, 0xf7},
+ },
+ ParentVoterSigData: []byte{0xa4, 0x25, 0xf1, 0xc9, 0x4, 0x55, 0xdb, 0x46, 0x51, 0x7a, 0x15, 0x8a, 0x10, 0x74, 0x9b, 0x1d, 0x1d, 0xbc, 0xb, 0x6d, 0x67, 0x33, 0x60, 0x21, 0x2e, 0x7b, 0xec, 0xae, 0xb3, 0x85, 0x14, 0x2b, 0x99, 0x1b, 0xc2, 0xd2, 0xa3, 0xfd, 0x59, 0x38, 0x13, 0x76, 0x21, 0x50, 0xc8, 0x57, 0xa4, 0xf8, 0xad, 0xd7, 0x2c, 0xce, 0x0, 0x45, 0x61, 0xc6, 0xb8, 0x98, 0x4a, 0x51, 0x92, 0xff, 0x2, 0x3, 0x2a, 0x87, 0xc1, 0x61, 0xbb, 0xda, 0x9, 0xab, 0x93, 0xc6, 0xb0, 0x60, 0x2e, 0xf9, 0x2f, 0xb9, 0x1f, 0x0, 0x58, 0xde, 0xd7, 0x86, 0x95, 0xbb, 0xfc, 0x85, 0x75, 0x1a, 0x52, 0x4c, 0x88, 0x7},
+ ProposerID: flow.Identifier{0x5, 0x55, 0x33, 0x7e, 0xf, 0x66, 0x1e, 0xc9, 0xb0, 0x7e, 0xbb, 0x69, 0x46, 0x8, 0x13, 0x16, 0xfa, 0x65, 0xc0, 0xba, 0xca, 0x6b, 0xd4, 0x70, 0x5b, 0xf6, 0x9d, 0x56, 0xa9, 0xf5, 0xb8, 0xa3},
+ ProposerSigData: []byte{0x94, 0x8e, 0x5, 0x8f, 0xbe, 0xa9, 0x6e, 0xf8, 0xbe, 0x43, 0x8, 0x49, 0xbf, 0x8b, 0x9d, 0x62, 0x47, 0xca, 0x57, 0x41, 0xbd, 0x81, 0x55, 0x23, 0x19, 0xa7, 0x5, 0x3a, 0xc8, 0x32, 0x51, 0x55, 0x2f, 0x62, 0x9f, 0xe2, 0xa0, 0x14, 0x30, 0xb0, 0xa9, 0x32, 0xa5, 0x1f, 0xa8, 0x27, 0xc1, 0x8a, 0x88, 0x8e, 0xa0, 0xd2, 0xcc, 0x67, 0xca, 0x18, 0x4d, 0xb4, 0x4b, 0x1a, 0xad, 0x62, 0x15, 0xb1, 0xa, 0xdd, 0x14, 0x1a, 0xf5, 0xa0, 0x1b, 0x56, 0x30, 0x7d, 0x70, 0x7c, 0x5b, 0xc6, 0xa8, 0xad, 0x74, 0x13, 0x1, 0xd, 0x10, 0xe2, 0x42, 0xe6, 0xc5, 0x96, 0xd4, 0x3c, 0xa8, 0xeb, 0x8a, 0x55},
+ }
+
+ case 425:
+ return flow.Header{
+ ChainID: dps.FlowTestnet,
+ ParentID: flow.Identifier{0x6a, 0xf2, 0x66, 0x21, 0xec, 0xa9, 0x2b, 0xab, 0xda, 0x2d, 0xf3, 0xeb, 0xcd, 0x2f, 0xe2, 0x69, 0x94, 0x6b, 0x3b, 0xf2, 0x8, 0x18, 0x35, 0x69, 0x25, 0x86, 0x30, 0xe6, 0x44, 0x86, 0x83, 0x1d},
+ Height: 425,
+ PayloadHash: flow.Identifier{0xca, 0x42, 0x4b, 0x9c, 0x56, 0xeb, 0xb7, 0x1d, 0xbf, 0xaa, 0x5d, 0x3b, 0xbd, 0xfa, 0x2f, 0xf6, 0x2b, 0x39, 0x7b, 0xb9, 0xcd, 0x4, 0x5, 0xfe, 0x8c, 0xd8, 0x9d, 0x5e, 0xe7, 0x74, 0x25, 0x7a},
+ Timestamp: time.Unix(0, 1621375483243086400).UTC(),
+ View: 427,
+ ParentVoterIDs: []flow.Identifier{
+ {0x45, 0x51, 0xbe, 0x34, 0xd9, 0xf7, 0xa9, 0x3b, 0x0, 0xd2, 0x87, 0xbd, 0x68, 0x3f, 0x7d, 0xd6, 0x34, 0x5e, 0x65, 0x90, 0x72, 0x40, 0x40, 0x5, 0x54, 0xfb, 0xdf, 0xa1, 0x69, 0x7d, 0x3b, 0xfa},
+ {0xd3, 0x5f, 0xac, 0xa6, 0x7a, 0xbc, 0x6, 0xc3, 0x34, 0xb1, 0xe5, 0xa7, 0x88, 0x23, 0x98, 0xda, 0xe9, 0xc1, 0xda, 0xd9, 0x13, 0xe5, 0x60, 0x9e, 0xe1, 0xd4, 0x63, 0xd5, 0x5a, 0x22, 0x44, 0xf7},
+ {0x5, 0x55, 0x33, 0x7e, 0xf, 0x66, 0x1e, 0xc9, 0xb0, 0x7e, 0xbb, 0x69, 0x46, 0x8, 0x13, 0x16, 0xfa, 0x65, 0xc0, 0xba, 0xca, 0x6b, 0xd4, 0x70, 0x5b, 0xf6, 0x9d, 0x56, 0xa9, 0xf5, 0xb8, 0xa3},
+ },
+ ParentVoterSigData: []byte{0xa9, 0x69, 0xb8, 0x57, 0xaf, 0xff, 0xd9, 0x4c, 0xd8, 0x94, 0x34, 0xc, 0xd8, 0xad, 0xa4, 0xb2, 0xe6, 0xd2, 0x8f, 0x18, 0x17, 0xa8, 0xa8, 0xc4, 0x80, 0x1e, 0x7a, 0x82, 0x90, 0xed, 0x58, 0x38, 0xf3, 0x2c, 0x4c, 0xa3, 0x31, 0xe3, 0x2b, 0x7e, 0x7c, 0x4b, 0xef, 0x5, 0x22, 0x9, 0xc4, 0xbc, 0xaf, 0x22, 0x1, 0x12, 0x4e, 0x37, 0xbb, 0xb1, 0xeb, 0xbb, 0x26, 0x9, 0x94, 0x6e, 0x7c, 0x49, 0xca, 0xba, 0xdc, 0x4f, 0x68, 0xbf, 0xd9, 0xb9, 0xc, 0x7e, 0x5d, 0xc1, 0x74, 0x1f, 0x4e, 0x5, 0xa2, 0xc5, 0x9f, 0x9b, 0x57, 0xb1, 0xfb, 0x75, 0x20, 0x95, 0xa3, 0x53, 0x7d, 0xa3, 0xa0, 0x3e},
+ ProposerID: flow.Identifier{0xd3, 0x5f, 0xac, 0xa6, 0x7a, 0xbc, 0x6, 0xc3, 0x34, 0xb1, 0xe5, 0xa7, 0x88, 0x23, 0x98, 0xda, 0xe9, 0xc1, 0xda, 0xd9, 0x13, 0xe5, 0x60, 0x9e, 0xe1, 0xd4, 0x63, 0xd5, 0x5a, 0x22, 0x44, 0xf7},
+ ProposerSigData: []byte{0xa5, 0x58, 0x17, 0xf4, 0x1c, 0x51, 0x43, 0xb7, 0x89, 0xd5, 0x65, 0xb4, 0x68, 0x21, 0xe2, 0x54, 0x78, 0xf4, 0xc2, 0xe7, 0x51, 0xd5, 0xd3, 0xe8, 0xbd, 0xbb, 0x1a, 0xb5, 0x90, 0xf8, 0xb, 0x5a, 0x32, 0xb8, 0x3d, 0xf6, 0xbc, 0xf, 0x10, 0x11, 0x71, 0x24, 0x6f, 0xe4, 0x77, 0x71, 0x8c, 0x32, 0x84, 0xfd, 0x52, 0xf5, 0x31, 0xc7, 0x1f, 0xe7, 0x4d, 0x43, 0xfe, 0xc9, 0xfa, 0x3b, 0x78, 0x67, 0xcc, 0xfe, 0x77, 0xd4, 0xfc, 0x42, 0x74, 0x36, 0x0, 0x9e, 0xf, 0x99, 0xf7, 0xaa, 0xb4, 0xc3, 0xf3, 0xff, 0x3a, 0x17, 0x6e, 0xc3, 0xd7, 0x3d, 0x41, 0xc2, 0x50, 0x7, 0x33, 0xb5, 0x17, 0x83},
+ }
+
+ default:
+ return flow.Header{}
+ }
+}
diff --git a/api/rosetta/errors.go b/api/rosetta/errors.go
new file mode 100755
index 0000000..db39d4d
--- /dev/null
+++ b/api/rosetta/errors.go
@@ -0,0 +1,441 @@
+// Copyright 2021 Optakt Labs OÜ
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+package rosetta
+
+import (
+ "errors"
+
+ "github.com/labstack/echo/v4"
+
+ "github.com/onflow/flow-go/model/flow"
+
+ "github.com/optakt/flow-rosetta/rosetta/configuration"
+ "github.com/optakt/flow-rosetta/rosetta/failure"
+ "github.com/optakt/flow-rosetta/rosetta/meta"
+)
+
+const (
+ invalidJSON = "request does not contain valid JSON-encoded body"
+
+ txInvalidOps = "transaction operations are invalid"
+
+ blockRetrieval = "unable to retrieve block"
+ balancesRetrieval = "unable to retrieve balances"
+ oldestRetrieval = "unable to retrieve oldest block"
+ currentRetrieval = "unable to retrieve current block"
+ txSubmission = "unable to submit transaction"
+ txRetrieval = "unable to retrieve transaction"
+ intentDetermination = "unable to determine transaction intent"
+ referenceBlockRetrieval = "unable to retrieve transaction reference block"
+ sequenceNumberRetrieval = "unable to retrieve account key sequence number"
+ txConstruction = "unable to construct transaction"
+ txParsing = "unable to parse transaction"
+ txSigning = "unable to sign transaction"
+ payloadHashing = "unable to hash signing payload"
+ txIdentifier = "unable to retrieve transaction identifier"
+)
+
+// Error represents an error as defined by the Rosetta API specification. It
+// contains an error definition, which has an error code, error message and
+// retriable flag that never change, as well as a description and a list of
+// details to provide more granular error information.
+// See: https://www.rosetta-api.org/docs/api_objects.html#error
+type Error struct {
+ meta.ErrorDefinition
+ Description string `json:"description"`
+ Details map[string]interface{} `json:"details,omitempty"`
+}
+
+type detailFunc func(map[string]interface{})
+
+func withError(err error) detailFunc {
+ return func(details map[string]interface{}) {
+ details["error"] = err.Error()
+ }
+}
+
+func withDetail(key string, val interface{}) detailFunc {
+ return func(details map[string]interface{}) {
+ details[key] = val
+ }
+}
+
+func withAddress(key string, val flow.Address) detailFunc {
+ return func(details map[string]interface{}) {
+ details[key] = val.Hex()
+ }
+}
+
+func rosettaError(definition meta.ErrorDefinition, description string, details ...detailFunc) Error {
+ dd := make(map[string]interface{})
+ for _, detail := range details {
+ detail(dd)
+ }
+ e := Error{
+ ErrorDefinition: definition,
+ Description: description,
+ Details: dd,
+ }
+ return e
+}
+
+func internal(description string, err error) Error {
+ return rosettaError(
+ configuration.ErrorInternal,
+ description,
+ withError(err),
+ )
+}
+
+func invalidEncoding(description string, err error) Error {
+ return rosettaError(
+ configuration.ErrorInvalidEncoding,
+ description,
+ withError(err),
+ )
+}
+
+func invalidFormat(description string, details ...detailFunc) Error {
+ return rosettaError(
+ configuration.ErrorInvalidFormat,
+ description,
+ details...,
+ )
+}
+
+func convertError(definition meta.ErrorDefinition, description failure.Description, details ...detailFunc) Error {
+ description.Fields.Iterate(func(key string, val interface{}) {
+ details = append(details, withDetail(key, val))
+ })
+ return rosettaError(definition, description.Text, details...)
+}
+
+func invalidNetwork(fail failure.InvalidNetwork) Error {
+ return convertError(
+ configuration.ErrorInvalidNetwork,
+ fail.Description,
+ withDetail("have_network", fail.HaveNetwork),
+ withDetail("want_network", fail.WantNetwork),
+ )
+}
+
+func invalidBlockchain(fail failure.InvalidBlockchain) Error {
+ return convertError(
+ configuration.ErrorInvalidNetwork,
+ fail.Description,
+ withDetail("have_blockchain", fail.HaveBlockchain),
+ withDetail("want_blockchain", fail.WantBlockchain),
+ )
+}
+
+func invalidAccount(fail failure.InvalidAccount) Error {
+ return convertError(
+ configuration.ErrorInvalidAccount,
+ fail.Description,
+ withDetail("address", fail.Address),
+ )
+}
+
+func invalidCurrency(fail failure.InvalidCurrency) Error {
+ return convertError(
+ configuration.ErrorInvalidCurrency,
+ fail.Description,
+ withDetail("symbol", fail.Symbol),
+ withDetail("decimals", fail.Decimals),
+ )
+}
+
+func invalidBlock(fail failure.InvalidBlock) Error {
+ return convertError(
+ configuration.ErrorInvalidBlock,
+ fail.Description,
+ )
+}
+
+func invalidTransaction(fail failure.InvalidTransaction) Error {
+ return convertError(
+ configuration.ErrorInvalidTransaction,
+ fail.Description,
+ withDetail("hash", fail.Hash),
+ )
+}
+
+func unknownCurrency(fail failure.UnknownCurrency) Error {
+ return convertError(
+ configuration.ErrorUnknownCurrency,
+ fail.Description,
+ withDetail("symbol", fail.Symbol),
+ withDetail("decimals", fail.Decimals),
+ )
+}
+
+func unknownBlock(fail failure.UnknownBlock) Error {
+ return convertError(
+ configuration.ErrorUnknownBlock,
+ fail.Description,
+ withDetail("index", fail.Index),
+ withDetail("hash", fail.Hash),
+ )
+}
+
+func unknownTransaction(fail failure.UnknownTransaction) Error {
+ return convertError(
+ configuration.ErrorUnknownTransaction,
+ fail.Description,
+ withDetail("hash", fail.Hash),
+ )
+}
+
+func invalidIntent(fail failure.InvalidIntent) Error {
+ return convertError(
+ configuration.ErrorInvalidIntent,
+ fail.Description,
+ )
+}
+
+func invalidAuthorizers(fail failure.InvalidAuthorizers) Error {
+ return convertError(
+ configuration.ErrorInvalidAuthorizers,
+ fail.Description,
+ withDetail("have_authorizers", fail.Have),
+ withDetail("want_authorizers", fail.Want),
+ )
+}
+
+func invalidSignatures(fail failure.InvalidSignatures) Error {
+ return convertError(
+ configuration.ErrorInvalidSignatures,
+ fail.Description,
+ withDetail("have_signatures", fail.Have),
+ withDetail("want_signatures", fail.Want),
+ )
+}
+
+func invalidPayer(fail failure.InvalidPayer) Error {
+ return convertError(
+ configuration.ErrorInvalidPayer,
+ fail.Description,
+ withAddress("have_payer", fail.Have),
+ withAddress("want_payer", fail.Want),
+ )
+}
+
+func invalidProposer(fail failure.InvalidProposer) Error {
+ return convertError(
+ configuration.ErrorInvalidProposer,
+ fail.Description,
+ withAddress("have_proposer", fail.Have),
+ withAddress("want_proposer", fail.Want),
+ )
+}
+
+func invalidScript(fail failure.InvalidScript) Error {
+ return convertError(
+ configuration.ErrorInvalidScript,
+ fail.Description,
+ withDetail("script", fail.Script),
+ )
+}
+
+func invalidArguments(fail failure.InvalidArguments) Error {
+ return convertError(
+ configuration.ErrorInvalidArguments,
+ fail.Description,
+ withDetail("have_arguments", fail.Have),
+ withDetail("want_arguments", fail.Want),
+ )
+}
+
+func invalidAmount(fail failure.InvalidAmount) Error {
+ return convertError(
+ configuration.ErrorInvalidAmount,
+ fail.Description,
+ withDetail("amount", fail.Amount),
+ )
+}
+
+func invalidReceiver(fail failure.InvalidReceiver) Error {
+ return convertError(
+ configuration.ErrorInvalidReceiver,
+ fail.Description,
+ withDetail("receiver", fail.Receiver),
+ )
+}
+
+func invalidSignature(fail failure.InvalidSignature) Error {
+ return convertError(
+ configuration.ErrorInvalidSignature,
+ fail.Description,
+ )
+}
+
+func invalidKey(fail failure.InvalidKey) Error {
+ return convertError(
+ configuration.ErrorInvalidKey,
+ fail.Description,
+ withDetail("height", fail.Height),
+ withAddress("account", fail.Address),
+ withDetail("index", fail.Index),
+ )
+}
+
+func invalidPayload(fail failure.InvalidPayload) Error {
+ return convertError(
+ configuration.ErrorInvalidPayload,
+ fail.Description,
+ withDetail("encoding", fail.Encoding),
+ )
+}
+
+// unpackError returns the HTTP status code and Rosetta Error for malformed JSON requests.
+func unpackError(err error) *echo.HTTPError {
+ return echo.NewHTTPError(statusBadRequest, invalidEncoding(invalidJSON, err))
+}
+
+// formatError returns the HTTP status code and Rosetta Error for requests
+// that did not pass validation.
+func formatError(err error) *echo.HTTPError {
+
+ var ibErr failure.InvalidBlockHash
+ if errors.As(err, &ibErr) {
+ return echo.NewHTTPError(statusBadRequest, invalidFormat(ibErr.Description.Text,
+ withDetail("want_length", ibErr.WantLength),
+ withDetail("have_length", ibErr.HaveLength),
+ ))
+ }
+ var iaErr failure.InvalidAccountAddress
+ if errors.As(err, &iaErr) {
+ return echo.NewHTTPError(statusBadRequest, invalidFormat(iaErr.Description.Text,
+ withDetail("want_length", iaErr.WantLength),
+ withDetail("have_length", iaErr.HaveLength),
+ ))
+ }
+ var itErr failure.InvalidTransactionHash
+ if errors.As(err, &itErr) {
+ return echo.NewHTTPError(statusBadRequest, invalidFormat(itErr.Description.Text,
+ withDetail("want_length", itErr.WantLength),
+ withDetail("have_length", itErr.HaveLength),
+ ))
+ }
+ var icErr failure.IncompleteBlock
+ if errors.As(err, &icErr) {
+ return echo.NewHTTPError(statusBadRequest, invalidFormat(icErr.Description.Text))
+ }
+ var inErr failure.InvalidNetwork
+ if errors.As(err, &inErr) {
+ return echo.NewHTTPError(statusUnprocessableEntity, invalidNetwork(inErr))
+ }
+ var iblErr failure.InvalidBlockchain
+ if errors.As(err, &iblErr) {
+ return echo.NewHTTPError(statusUnprocessableEntity, invalidBlockchain(iblErr))
+ }
+
+ return echo.NewHTTPError(statusBadRequest, invalidFormat(err.Error()))
+}
+
+// apiError returns the HTTP status code and Rosetta Error for various errors
+// occurred during request processing.
+func apiError(description string, err error) *echo.HTTPError {
+
+ // Common errors, found both in Data and Construction API.
+ var inErr failure.InvalidNetwork
+ if errors.As(err, &inErr) {
+ return echo.NewHTTPError(statusUnprocessableEntity, invalidNetwork(inErr))
+ }
+ var ibErr failure.InvalidBlock
+ if errors.As(err, &ibErr) {
+ return echo.NewHTTPError(statusUnprocessableEntity, invalidBlock(ibErr))
+ }
+ var ubErr failure.UnknownBlock
+ if errors.As(err, &ubErr) {
+ return echo.NewHTTPError(statusUnprocessableEntity, unknownBlock(ubErr))
+ }
+ var iaErr failure.InvalidAccount
+ if errors.As(err, &iaErr) {
+ return echo.NewHTTPError(statusUnprocessableEntity, invalidAccount(iaErr))
+ }
+ var icErr failure.InvalidCurrency
+ if errors.As(err, &icErr) {
+ return echo.NewHTTPError(statusUnprocessableEntity, invalidCurrency(icErr))
+ }
+ var ucErr failure.UnknownCurrency
+ if errors.As(err, &ucErr) {
+ return echo.NewHTTPError(statusUnprocessableEntity, unknownCurrency(ucErr))
+ }
+ var itErr failure.InvalidTransaction
+ if errors.As(err, &itErr) {
+ return echo.NewHTTPError(statusUnprocessableEntity, invalidTransaction(itErr))
+ }
+ var utErr failure.UnknownTransaction
+ if errors.As(err, &utErr) {
+ return echo.NewHTTPError(statusUnprocessableEntity, unknownTransaction(utErr))
+ }
+
+ // Construction API specific errors.
+ var iautErr failure.InvalidAuthorizers
+ if errors.As(err, &iaErr) {
+ return echo.NewHTTPError(statusUnprocessableEntity, invalidAuthorizers(iautErr))
+ }
+ var ipyErr failure.InvalidPayer
+ if errors.As(err, &ipyErr) {
+ return echo.NewHTTPError(statusUnprocessableEntity, invalidPayer(ipyErr))
+ }
+ var iprErr failure.InvalidProposer
+ if errors.As(err, &iprErr) {
+ return echo.NewHTTPError(statusUnprocessableEntity, invalidProposer(iprErr))
+ }
+ var isgErr failure.InvalidSignature
+ if errors.As(err, &isgErr) {
+ return echo.NewHTTPError(statusUnprocessableEntity, invalidSignature(isgErr))
+ }
+ var isgsErr failure.InvalidSignatures
+ if errors.As(err, &isgsErr) {
+ return echo.NewHTTPError(statusUnprocessableEntity, invalidSignatures(isgsErr))
+ }
+ var opErr failure.InvalidOperations
+ if errors.As(err, &opErr) {
+ return echo.NewHTTPError(statusUnprocessableEntity, invalidFormat(txInvalidOps))
+ }
+ var intErr failure.InvalidIntent
+ if errors.As(err, &intErr) {
+ return echo.NewHTTPError(statusUnprocessableEntity, invalidIntent(intErr))
+ }
+ var ikErr failure.InvalidKey
+ if errors.As(err, &ipyErr) {
+ return echo.NewHTTPError(statusUnprocessableEntity, invalidKey(ikErr))
+ }
+ var isErr failure.InvalidScript
+ if errors.As(err, &isErr) {
+ return echo.NewHTTPError(statusUnprocessableEntity, invalidScript(isErr))
+ }
+ var iargErr failure.InvalidArguments
+ if errors.As(err, &iargErr) {
+ return echo.NewHTTPError(statusUnprocessableEntity, invalidArguments(iargErr))
+ }
+ var imErr failure.InvalidAmount
+ if errors.As(err, &imErr) {
+ return echo.NewHTTPError(statusUnprocessableEntity, invalidAmount(imErr))
+ }
+ var irErr failure.InvalidReceiver
+ if errors.As(err, &irErr) {
+ return echo.NewHTTPError(statusUnprocessableEntity, invalidReceiver(irErr))
+ }
+ var iplErr failure.InvalidPayload
+ if errors.As(err, &iplErr) {
+ return echo.NewHTTPError(statusUnprocessableEntity, invalidPayload(iplErr))
+ }
+
+ return echo.NewHTTPError(statusInternalServerError, internal(description, err))
+}
diff --git a/api/rosetta/hash.go b/api/rosetta/hash.go
new file mode 100755
index 0000000..81fb2af
--- /dev/null
+++ b/api/rosetta/hash.go
@@ -0,0 +1,50 @@
+// Copyright 2021 Optakt Labs OÜ
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+package rosetta
+
+import (
+ "github.com/labstack/echo/v4"
+
+ "github.com/optakt/flow-rosetta/rosetta/request"
+ "github.com/optakt/flow-rosetta/rosetta/response"
+)
+
+// Hash implements the /construction/hash endpoint of the Rosetta Construction API.
+// It returns the transaction ID of a signed transaction.
+// See https://www.rosetta-api.org/docs/ConstructionApi.html#constructionhash
+func (c *Construction) Hash(ctx echo.Context) error {
+
+ var req request.Hash
+ err := ctx.Bind(&req)
+ if err != nil {
+ return unpackError(err)
+ }
+
+ err = c.validate.Request(req)
+ if err != nil {
+ return formatError(err)
+ }
+
+ rosTxID, err := c.transact.TransactionIdentifier(req.SignedTransaction)
+ if err != nil {
+ return apiError(txIdentifier, err)
+ }
+
+ res := response.Hash{
+ TransactionID: rosTxID,
+ }
+
+ return ctx.JSON(statusOK, res)
+}
diff --git a/api/rosetta/length.go b/api/rosetta/length.go
new file mode 100755
index 0000000..d74f126
--- /dev/null
+++ b/api/rosetta/length.go
@@ -0,0 +1,25 @@
+// Copyright 2021 Optakt Labs OÜ
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+package rosetta
+
+import (
+ "github.com/onflow/flow-go/model/flow"
+)
+
+// Sizes are multiplied by two because hex-encoded strings use two characters for every byte.
+const (
+ HexIDSize = 2 * len(flow.ZeroID)
+ HexAddressSize = 2 * flow.AddressLength
+)
diff --git a/api/rosetta/metadata.go b/api/rosetta/metadata.go
new file mode 100755
index 0000000..484f8c2
--- /dev/null
+++ b/api/rosetta/metadata.go
@@ -0,0 +1,66 @@
+// Copyright 2021 Optakt Labs OÜ
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+package rosetta
+
+import (
+ "github.com/labstack/echo/v4"
+
+ "github.com/optakt/flow-rosetta/rosetta/object"
+ "github.com/optakt/flow-rosetta/rosetta/request"
+ "github.com/optakt/flow-rosetta/rosetta/response"
+)
+
+// Metadata implements the /construction/metadata endpoint of the Rosetta Construction API.
+// Metadata endpoint returns information required for constructing the transaction.
+// For Flow, that information includes the reference block and sequence number. Reference block
+// is the last indexed block, and is used to track transaction expiration. Sequence number is
+// the proposer account's public key sequence number. Sequence number is incremented for each
+// transaction and is used to prevent replay attacks.
+// See https://www.rosetta-api.org/docs/ConstructionApi.html#constructionmetadata
+func (c *Construction) Metadata(ctx echo.Context) error {
+
+ var req request.Metadata
+ err := ctx.Bind(&req)
+ if err != nil {
+ return unpackError(err)
+ }
+
+ err = c.validate.Request(req)
+ if err != nil {
+ return formatError(err)
+ }
+
+ current, _, err := c.retrieve.Current()
+ if err != nil {
+ return apiError(referenceBlockRetrieval, err)
+ }
+
+ sequence, err := c.retrieve.Sequence(current, req.Options.AccountID, 0)
+ if err != nil {
+ return apiError(sequenceNumberRetrieval, err)
+ }
+
+ // In the `parse` endpoint, we parse a transaction to produce the original metadata (and operations).
+ // Since we can only deduce the block hash from the transaction, we will omit the block height from
+ // the identifier here, to keep the data identical.
+ res := response.Metadata{
+ Metadata: object.Metadata{
+ CurrentBlockID: current,
+ SequenceNumber: sequence,
+ },
+ }
+
+ return ctx.JSON(statusOK, res)
+}
diff --git a/api/rosetta/networks.go b/api/rosetta/networks.go
new file mode 100755
index 0000000..edc7762
--- /dev/null
+++ b/api/rosetta/networks.go
@@ -0,0 +1,42 @@
+// Copyright 2021 Optakt Labs OÜ
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+package rosetta
+
+import (
+ "github.com/labstack/echo/v4"
+
+ "github.com/optakt/flow-rosetta/rosetta/identifier"
+ "github.com/optakt/flow-rosetta/rosetta/request"
+ "github.com/optakt/flow-rosetta/rosetta/response"
+)
+
+// Networks implements the /network/list endpoint of the Rosetta Data API.
+// See https://www.rosetta-api.org/docs/NetworkApi.html#networklist
+func (d *Data) Networks(ctx echo.Context) error {
+
+ // Decode the network list request from the HTTP request JSON body.
+ var req request.Networks
+ err := ctx.Bind(&req)
+ if err != nil {
+ return unpackError(err)
+ }
+
+ // Get the network we are running on from the configuration.
+ res := response.Networks{
+ NetworkIDs: []identifier.Network{d.config.Network()},
+ }
+
+ return ctx.JSON(statusOK, res)
+}
diff --git a/api/rosetta/networks_integration_test.go b/api/rosetta/networks_integration_test.go
new file mode 100755
index 0000000..c0dca84
--- /dev/null
+++ b/api/rosetta/networks_integration_test.go
@@ -0,0 +1,58 @@
+// Copyright 2021 Optakt Labs OÜ
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+//go:build integration
+// +build integration
+
+package rosetta_test
+
+import (
+ "encoding/json"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+
+ "github.com/optakt/flow-dps/models/dps"
+ "github.com/optakt/flow-rosetta/rosetta/request"
+ "github.com/optakt/flow-rosetta/rosetta/response"
+)
+
+func TestAPI_Networks(t *testing.T) {
+ // TODO: Repair integration tests
+ // See https://github.com/optakt/flow-dps/issues/333
+ t.Skip("integration tests disabled until new snapshot is generated")
+
+ db := setupDB(t)
+ api := setupAPI(t, db)
+
+ // network request is basically an empty payload at the moment,
+ // there is a 'metadata' object that we're ignoring;
+ // but we can have the scaffolding here in case something changes
+
+ var netReq request.Networks
+
+ rec, ctx, err := setupRecorder(listEndpoint, netReq)
+ require.NoError(t, err)
+
+ err = api.Networks(ctx)
+ assert.NoError(t, err)
+
+ var res response.Networks
+ require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &res))
+
+ require.Len(t, res.NetworkIDs, 1)
+ assert.Equal(t, res.NetworkIDs[0].Blockchain, dps.FlowBlockchain)
+ assert.Equal(t, res.NetworkIDs[0].Network, dps.FlowTestnet.String())
+}
diff --git a/api/rosetta/options.go b/api/rosetta/options.go
new file mode 100755
index 0000000..3f2099d
--- /dev/null
+++ b/api/rosetta/options.go
@@ -0,0 +1,57 @@
+// Copyright 2021 Optakt Labs OÜ
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+package rosetta
+
+import (
+ "github.com/labstack/echo/v4"
+
+ "github.com/optakt/flow-rosetta/rosetta/request"
+ "github.com/optakt/flow-rosetta/rosetta/response"
+)
+
+// Options implements the /network/options endpoint of the Rosetta Data API.
+// See https://www.rosetta-api.org/docs/NetworkApi.html#networkoptions
+func (d *Data) Options(ctx echo.Context) error {
+
+ // Decode the network list request from the HTTP request JSON body.
+ var req request.Options
+ err := ctx.Bind(&req)
+ if err != nil {
+ return unpackError(err)
+ }
+
+ err = d.validate.Request(req)
+ if err != nil {
+ return formatError(err)
+ }
+
+ // Create the allow object, which is native to the response.
+ allow := response.OptionsAllow{
+ OperationStatuses: d.config.Statuses(),
+ OperationTypes: d.config.Operations(),
+ Errors: d.config.Errors(),
+ HistoricalBalanceLookup: true,
+ CallMethods: []string{},
+ BalanceExemptions: []struct{}{},
+ MempoolCoins: false,
+ }
+
+ res := response.Options{
+ Version: d.config.Version(),
+ Allow: allow,
+ }
+
+ return ctx.JSON(statusOK, res)
+}
diff --git a/api/rosetta/options_integration_test.go b/api/rosetta/options_integration_test.go
new file mode 100755
index 0000000..15ed321
--- /dev/null
+++ b/api/rosetta/options_integration_test.go
@@ -0,0 +1,352 @@
+// Copyright 2021 Optakt Labs OÜ
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+//go:build integration
+// +build integration
+
+package rosetta_test
+
+import (
+ "encoding/json"
+ "net/http"
+ "regexp"
+ "testing"
+
+ "github.com/labstack/echo/v4"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+
+ "github.com/optakt/flow-dps/models/dps"
+ "github.com/optakt/flow-rosetta/api/rosetta"
+ "github.com/optakt/flow-rosetta/rosetta/configuration"
+ "github.com/optakt/flow-rosetta/rosetta/identifier"
+ "github.com/optakt/flow-rosetta/rosetta/request"
+ "github.com/optakt/flow-rosetta/rosetta/response"
+)
+
+func TestAPI_Options(t *testing.T) {
+ // TODO: Repair integration tests
+ // See https://github.com/optakt/flow-dps/issues/333
+ t.Skip("integration tests disabled until new snapshot is generated")
+
+ db := setupDB(t)
+ api := setupAPI(t, db)
+
+ const wantErrorCount = 23
+
+ // verify version string is in the format of x.y.z
+ versionRe := regexp.MustCompile(`\d+\.\d+\.\d+`)
+
+ request := request.Options{
+ NetworkID: defaultNetwork(),
+ }
+
+ rec, ctx, err := setupRecorder(optionsEndpoint, request)
+
+ err = api.Options(ctx)
+ require.NoError(t, err)
+
+ assert.NoError(t, err)
+
+ assert.Equal(t, http.StatusOK, rec.Result().StatusCode)
+
+ var options response.Options
+ require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &options))
+
+ assert.Regexp(t, versionRe, options.Version.RosettaVersion)
+ assert.Regexp(t, versionRe, options.Version.NodeVersion)
+ assert.Regexp(t, versionRe, options.Version.MiddlewareVersion)
+
+ assert.True(t, options.Allow.HistoricalBalanceLookup)
+
+ require.Len(t, options.Allow.OperationStatuses, 1)
+
+ status := options.Allow.OperationStatuses[0]
+ assert.Equal(t, status.Status, dps.StatusCompleted)
+ assert.True(t, status.Successful)
+
+ require.Len(t, options.Allow.OperationTypes, 1)
+ assert.Equal(t, options.Allow.OperationTypes[0], dps.OperationTransfer)
+
+ require.Len(t, options.Allow.Errors, wantErrorCount)
+
+ for i := uint(0); i < wantErrorCount; i++ {
+ rosettaErr := options.Allow.Errors[i]
+
+ expectedCode := i + 1 // error codes start from 1
+ assert.Equal(t, expectedCode, rosettaErr.Code)
+
+ switch expectedCode {
+ case configuration.ErrorInternal.Code:
+ assert.Equal(t, configuration.ErrorInternal.Message, rosettaErr.Message)
+ assert.Equal(t, configuration.ErrorInternal.Retriable, rosettaErr.Retriable)
+
+ case configuration.ErrorInvalidEncoding.Code:
+ assert.Equal(t, configuration.ErrorInvalidEncoding.Message, rosettaErr.Message)
+ assert.Equal(t, configuration.ErrorInvalidEncoding.Retriable, rosettaErr.Retriable)
+
+ case configuration.ErrorInvalidFormat.Code:
+ assert.Equal(t, configuration.ErrorInvalidFormat.Message, rosettaErr.Message)
+ assert.Equal(t, configuration.ErrorInvalidFormat.Retriable, rosettaErr.Retriable)
+
+ case configuration.ErrorInvalidNetwork.Code:
+ assert.Equal(t, configuration.ErrorInvalidNetwork.Message, rosettaErr.Message)
+ assert.Equal(t, configuration.ErrorInvalidNetwork.Retriable, rosettaErr.Retriable)
+
+ case configuration.ErrorInvalidAccount.Code:
+ assert.Equal(t, configuration.ErrorInvalidAccount.Message, rosettaErr.Message)
+ assert.Equal(t, configuration.ErrorInvalidAccount.Retriable, rosettaErr.Retriable)
+
+ case configuration.ErrorInvalidCurrency.Code:
+ assert.Equal(t, configuration.ErrorInvalidCurrency.Message, rosettaErr.Message)
+ assert.Equal(t, configuration.ErrorInvalidCurrency.Retriable, rosettaErr.Retriable)
+
+ case configuration.ErrorInvalidBlock.Code:
+ assert.Equal(t, configuration.ErrorInvalidBlock.Message, rosettaErr.Message)
+ assert.Equal(t, configuration.ErrorInvalidBlock.Retriable, rosettaErr.Retriable)
+
+ case configuration.ErrorInvalidTransaction.Code:
+ assert.Equal(t, configuration.ErrorInvalidTransaction.Message, rosettaErr.Message)
+ assert.Equal(t, configuration.ErrorInvalidTransaction.Retriable, rosettaErr.Retriable)
+
+ case configuration.ErrorUnknownBlock.Code:
+ assert.Equal(t, configuration.ErrorUnknownBlock.Message, rosettaErr.Message)
+ assert.Equal(t, configuration.ErrorUnknownBlock.Retriable, rosettaErr.Retriable)
+
+ case configuration.ErrorUnknownCurrency.Code:
+ assert.Equal(t, configuration.ErrorUnknownCurrency.Message, rosettaErr.Message)
+ assert.Equal(t, configuration.ErrorUnknownCurrency.Retriable, rosettaErr.Retriable)
+
+ case configuration.ErrorUnknownTransaction.Code:
+ assert.Equal(t, configuration.ErrorUnknownTransaction.Message, rosettaErr.Message)
+ assert.Equal(t, configuration.ErrorUnknownTransaction.Retriable, rosettaErr.Retriable)
+
+ case configuration.ErrorInvalidIntent.Code:
+ assert.Equal(t, configuration.ErrorInvalidIntent.Message, rosettaErr.Message)
+ assert.Equal(t, configuration.ErrorInvalidIntent.Retriable, rosettaErr.Retriable)
+
+ case configuration.ErrorInvalidAuthorizers.Code:
+ assert.Equal(t, configuration.ErrorInvalidAuthorizers.Message, rosettaErr.Message)
+ assert.Equal(t, configuration.ErrorInvalidAuthorizers.Retriable, rosettaErr.Retriable)
+
+ case configuration.ErrorInvalidPayer.Code:
+ assert.Equal(t, configuration.ErrorInvalidPayer.Message, rosettaErr.Message)
+ assert.Equal(t, configuration.ErrorInvalidPayer.Retriable, rosettaErr.Retriable)
+
+ case configuration.ErrorInvalidProposer.Code:
+ assert.Equal(t, configuration.ErrorInvalidProposer.Message, rosettaErr.Message)
+ assert.Equal(t, configuration.ErrorInvalidProposer.Retriable, rosettaErr.Retriable)
+
+ case configuration.ErrorInvalidScript.Code:
+ assert.Equal(t, configuration.ErrorInvalidScript.Message, rosettaErr.Message)
+ assert.Equal(t, configuration.ErrorInvalidScript.Retriable, rosettaErr.Retriable)
+
+ case configuration.ErrorInvalidArguments.Code:
+ assert.Equal(t, configuration.ErrorInvalidArguments.Message, rosettaErr.Message)
+ assert.Equal(t, configuration.ErrorInvalidArguments.Retriable, rosettaErr.Retriable)
+
+ case configuration.ErrorInvalidAmount.Code:
+ assert.Equal(t, configuration.ErrorInvalidAmount.Message, rosettaErr.Message)
+ assert.Equal(t, configuration.ErrorInvalidAmount.Retriable, rosettaErr.Retriable)
+
+ case configuration.ErrorInvalidReceiver.Code:
+ assert.Equal(t, configuration.ErrorInvalidReceiver.Message, rosettaErr.Message)
+ assert.Equal(t, configuration.ErrorInvalidReceiver.Retriable, rosettaErr.Retriable)
+
+ case configuration.ErrorInvalidSignature.Code:
+ assert.Equal(t, configuration.ErrorInvalidSignature.Message, rosettaErr.Message)
+ assert.Equal(t, configuration.ErrorInvalidSignature.Retriable, rosettaErr.Retriable)
+
+ case configuration.ErrorInvalidKey.Code:
+ assert.Equal(t, configuration.ErrorInvalidKey.Message, rosettaErr.Message)
+ assert.Equal(t, configuration.ErrorInvalidKey.Retriable, rosettaErr.Retriable)
+
+ case configuration.ErrorInvalidPayload.Code:
+ assert.Equal(t, configuration.ErrorInvalidPayload.Message, rosettaErr.Message)
+ assert.Equal(t, configuration.ErrorInvalidPayload.Retriable, rosettaErr.Retriable)
+
+ case configuration.ErrorInvalidSignatures.Code:
+ assert.Equal(t, configuration.ErrorInvalidSignatures.Message, rosettaErr.Message)
+ assert.Equal(t, configuration.ErrorInvalidSignatures.Retriable, rosettaErr.Retriable)
+
+ default:
+ t.Errorf("unknown rosetta error received: (code: %v, message: '%v', retriable: %v", rosettaErr.Code, rosettaErr.Message, rosettaErr.Retriable)
+ }
+ }
+}
+
+func TestAPI_OptionsHandlesErrors(t *testing.T) {
+ // TODO: Repair integration tests
+ // See https://github.com/optakt/flow-dps/issues/333
+ t.Skip("integration tests disabled until new snapshot is generated")
+
+ db := setupDB(t)
+ api := setupAPI(t, db)
+
+ tests := []struct {
+ name string
+
+ request request.Options
+
+ checkError assert.ErrorAssertionFunc
+ }{
+ {
+ name: "missing blockchain",
+ request: request.Options{
+ NetworkID: identifier.Network{
+ Blockchain: "",
+ Network: dps.FlowTestnet.String(),
+ },
+ },
+
+ checkError: checkRosettaError(http.StatusBadRequest, configuration.ErrorInvalidFormat),
+ },
+ {
+ name: "invalid blockchain",
+ request: request.Options{
+ NetworkID: identifier.Network{
+ Blockchain: invalidBlockchain,
+ Network: dps.FlowTestnet.String(),
+ },
+ },
+
+ checkError: checkRosettaError(http.StatusUnprocessableEntity, configuration.ErrorInvalidNetwork),
+ },
+ {
+ name: "missing network",
+ request: request.Options{
+ NetworkID: identifier.Network{
+ Blockchain: dps.FlowBlockchain,
+ Network: "",
+ },
+ },
+
+ checkError: checkRosettaError(http.StatusBadRequest, configuration.ErrorInvalidFormat),
+ },
+ {
+ name: "invalid network",
+ request: request.Options{
+ NetworkID: identifier.Network{
+ Blockchain: dps.FlowBlockchain,
+ Network: invalidNetwork,
+ },
+ },
+
+ checkError: checkRosettaError(http.StatusUnprocessableEntity, configuration.ErrorInvalidNetwork),
+ },
+ }
+
+ for _, test := range tests {
+
+ test := test
+ t.Run(test.name, func(t *testing.T) {
+
+ t.Parallel()
+
+ _, ctx, err := setupRecorder(optionsEndpoint, test.request)
+ require.NoError(t, err)
+
+ err = api.Options(ctx)
+ test.checkError(t, err)
+ })
+ }
+
+}
+
+func TestAPI_OptionsHandlesMalformedRequest(t *testing.T) {
+ // TODO: Repair integration tests
+ // See https://github.com/optakt/flow-dps/issues/333
+ t.Skip("integration tests disabled until new snapshot is generated")
+
+ db := setupDB(t)
+ api := setupAPI(t, db)
+
+ const (
+ // wrong type for 'network' field
+ wrongFieldType = `{
+ "network_identifier": {
+ "blockchain": "flow",
+ "network": 99
+ }
+ }`
+
+ // malformed JSON - unclosed bracket
+ unclosedBracket = `{
+ "network_identifier": {
+ "blockchain": "flow",
+ "network": "flow-testnet"
+ }`
+
+ validPayload = `{
+ "network_identifier": {
+ "blockchain": "flow",
+ "network": "flow-testnet"
+ }
+ }`
+ )
+
+ tests := []struct {
+ name string
+
+ payload []byte
+
+ prepare func(*http.Request)
+ }{
+ {
+ name: "invalid options input types",
+ payload: []byte(wrongFieldType),
+ prepare: func(req *http.Request) {
+ req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON)
+ },
+ },
+ {
+ name: "invalid options json format",
+ payload: []byte(unclosedBracket),
+ prepare: func(req *http.Request) {
+ req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON)
+ },
+ },
+ {
+ name: "valid options payload with no MIME type",
+ payload: []byte(validPayload),
+ prepare: func(req *http.Request) {
+ req.Header.Set(echo.HeaderContentType, "")
+ },
+ },
+ }
+
+ for _, test := range tests {
+
+ test := test
+ t.Run(test.name, func(t *testing.T) {
+
+ t.Parallel()
+
+ _, ctx, err := setupRecorder(optionsEndpoint, test.payload, test.prepare)
+ require.NoError(t, err)
+
+ err = api.Options(ctx)
+ assert.Error(t, err)
+
+ echoErr, ok := err.(*echo.HTTPError)
+ require.True(t, ok)
+
+ assert.Equal(t, http.StatusBadRequest, echoErr.Code)
+ gotErr, ok := echoErr.Message.(rosetta.Error)
+ require.True(t, ok)
+
+ assert.Equal(t, configuration.ErrorInvalidEncoding, gotErr.ErrorDefinition)
+ })
+ }
+}
diff --git a/api/rosetta/parse.go b/api/rosetta/parse.go
new file mode 100755
index 0000000..8b732b7
--- /dev/null
+++ b/api/rosetta/parse.go
@@ -0,0 +1,76 @@
+// Copyright 2021 Optakt Labs OÜ
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+package rosetta
+
+import (
+ "github.com/labstack/echo/v4"
+
+ "github.com/optakt/flow-rosetta/rosetta/object"
+ "github.com/optakt/flow-rosetta/rosetta/request"
+ "github.com/optakt/flow-rosetta/rosetta/response"
+)
+
+// Parse implements the /construction/parse endpoint of the Rosetta Construction API.
+// Parse endpoint parses both signed and unsigned transactions to understand the
+// transaction's intent. Endpoint returns the list of operations, any relevant metadata,
+// and, in the case of signed transaction, the list of signers.
+// See https://www.rosetta-api.org/docs/ConstructionApi.html#constructionparse
+func (c *Construction) Parse(ctx echo.Context) error {
+
+ var req request.Parse
+ err := ctx.Bind(&req)
+ if err != nil {
+ return unpackError(err)
+ }
+
+ err = c.validate.Request(req)
+ if err != nil {
+ return formatError(err)
+ }
+
+ parse, err := c.transact.Parse(req.Transaction)
+ if err != nil {
+ return apiError(txParsing, err)
+ }
+
+ refBlockID, err := parse.BlockID()
+ if err != nil {
+ return apiError(txParsing, err)
+ }
+
+ signers, err := parse.Signers()
+ if err != nil {
+ return apiError(txParsing, err)
+ }
+
+ operations, err := parse.Operations()
+ if err != nil {
+ return apiError(txParsing, err)
+ }
+
+ sequence := parse.Sequence()
+ metadata := object.Metadata{
+ CurrentBlockID: refBlockID,
+ SequenceNumber: sequence,
+ }
+
+ res := response.Parse{
+ Operations: operations,
+ SignerIDs: signers,
+ Metadata: metadata,
+ }
+
+ return ctx.JSON(statusOK, res)
+}
diff --git a/api/rosetta/payloads.go b/api/rosetta/payloads.go
new file mode 100755
index 0000000..1ad5e9d
--- /dev/null
+++ b/api/rosetta/payloads.go
@@ -0,0 +1,83 @@
+// Copyright 2021 Optakt Labs OÜ
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+package rosetta
+
+import (
+ "github.com/labstack/echo/v4"
+
+ "github.com/optakt/flow-rosetta/rosetta/identifier"
+ "github.com/optakt/flow-rosetta/rosetta/object"
+ "github.com/optakt/flow-rosetta/rosetta/request"
+ "github.com/optakt/flow-rosetta/rosetta/response"
+)
+
+// Payloads implements the /construction/payloads endpoint of the Rosetta Construction API.
+// It receives an array of operations and all other relevant information required to construct
+// an unsigned transaction. Operations must deterministically describe the intent of the
+// transaction. Besides the unsigned transaction text, this endpoint also returns the list
+// of payloads that should be signed.
+// See https://www.rosetta-api.org/docs/ConstructionApi.html#constructionpayloads
+func (c *Construction) Payloads(ctx echo.Context) error {
+
+ var req request.Payloads
+ err := ctx.Bind(&req)
+ if err != nil {
+ return unpackError(err)
+ }
+
+ err = c.validate.Request(req)
+ if err != nil {
+ return formatError(err)
+ }
+
+ // Metadata object is the response from our metadata endpoint. Thus, the object
+ // should be okay, but let's validate it anyway.
+ err = c.validate.CompleteBlockID(req.Metadata.CurrentBlockID)
+ if err != nil {
+ return formatError(err)
+ }
+
+ intent, err := c.transact.DeriveIntent(req.Operations)
+ if err != nil {
+ return apiError(intentDetermination, err)
+ }
+
+ unsigned, err := c.transact.CompileTransaction(req.Metadata.CurrentBlockID, intent, req.Metadata.SequenceNumber)
+ if err != nil {
+ return apiError(txConstruction, err)
+ }
+
+ sender := identifier.Account{
+ Address: intent.From.String(),
+ }
+ algo, hash, err := c.transact.HashPayload(req.Metadata.CurrentBlockID, unsigned, sender)
+ if err != nil {
+ return apiError(payloadHashing, err)
+ }
+
+ // We only support a single signer at the moment, so the account only needs to sign the transaction envelope.
+ res := response.Payloads{
+ Transaction: unsigned,
+ Payloads: []object.SigningPayload{
+ {
+ AccountID: identifier.Account{Address: intent.From.Hex()},
+ HexBytes: hash,
+ SignatureType: algo,
+ },
+ },
+ }
+
+ return ctx.JSON(statusOK, res)
+}
diff --git a/api/rosetta/preprocess.go b/api/rosetta/preprocess.go
new file mode 100755
index 0000000..8e1f862
--- /dev/null
+++ b/api/rosetta/preprocess.go
@@ -0,0 +1,59 @@
+// Copyright 2021 Optakt Labs OÜ
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+package rosetta
+
+import (
+ "github.com/labstack/echo/v4"
+
+ "github.com/optakt/flow-rosetta/rosetta/identifier"
+ "github.com/optakt/flow-rosetta/rosetta/object"
+ "github.com/optakt/flow-rosetta/rosetta/request"
+ "github.com/optakt/flow-rosetta/rosetta/response"
+)
+
+// Preprocess implements the /construction/preprocess endpoint of the Rosetta Construction API.
+// Preprocess receives a list of operations that should deterministically specify the
+// intent of the transaction. Preprocess endpoint returns the `options` object that
+// will be sent **unmodified** to /construction/metadata, effectively creating the metadata
+// request.
+// See https://www.rosetta-api.org/docs/ConstructionApi.html#constructionpreprocess
+func (c *Construction) Preprocess(ctx echo.Context) error {
+
+ var req request.Preprocess
+ err := ctx.Bind(&req)
+ if err != nil {
+ return unpackError(err)
+ }
+
+ err = c.validate.Request(req)
+ if err != nil {
+ return formatError(err)
+ }
+
+ intent, err := c.transact.DeriveIntent(req.Operations)
+ if err != nil {
+ return apiError(intentDetermination, err)
+ }
+
+ res := response.Preprocess{
+ Options: object.Options{
+ AccountID: identifier.Account{
+ Address: intent.From.Hex(),
+ },
+ },
+ }
+
+ return ctx.JSON(statusOK, res)
+}
diff --git a/api/rosetta/retriever.go b/api/rosetta/retriever.go
new file mode 100755
index 0000000..f11012d
--- /dev/null
+++ b/api/rosetta/retriever.go
@@ -0,0 +1,31 @@
+// Copyright 2021 Optakt Labs OÜ
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+package rosetta
+
+import (
+ "time"
+
+ "github.com/optakt/flow-rosetta/rosetta/identifier"
+ "github.com/optakt/flow-rosetta/rosetta/object"
+)
+
+type Retriever interface {
+ Oldest() (identifier.Block, time.Time, error)
+ Current() (identifier.Block, time.Time, error)
+ Block(rosBlockID identifier.Block) (*object.Block, []identifier.Transaction, error)
+ Transaction(rosBlockID identifier.Block, rosTxID identifier.Transaction) (*object.Transaction, error)
+ Balances(rosBlockID identifier.Block, rosAccountID identifier.Account, rosCurrencies []identifier.Currency) (identifier.Block, []object.Amount, error)
+ Sequence(rosBlockID identifier.Block, rosAccountID identifier.Account, index int) (uint64, error)
+}
diff --git a/api/rosetta/status.go b/api/rosetta/status.go
new file mode 100755
index 0000000..e076a90
--- /dev/null
+++ b/api/rosetta/status.go
@@ -0,0 +1,58 @@
+// Copyright 2021 Optakt Labs OÜ
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+package rosetta
+
+import (
+ "github.com/labstack/echo/v4"
+
+ "github.com/optakt/flow-rosetta/rosetta/request"
+ "github.com/optakt/flow-rosetta/rosetta/response"
+)
+
+// Status implements the /network/status endpoint of the Rosetta Data API.
+// See https://www.rosetta-api.org/docs/NetworkApi.html#networkstatus
+func (d *Data) Status(ctx echo.Context) error {
+
+ var req request.Status
+ err := ctx.Bind(&req)
+ if err != nil {
+ return unpackError(err)
+ }
+
+ err = d.validate.Request(req)
+ if err != nil {
+ return formatError(err)
+ }
+
+ oldest, _, err := d.retrieve.Oldest()
+ if err != nil {
+ return apiError(oldestRetrieval, err)
+ }
+
+ current, timestamp, err := d.retrieve.Current()
+ if err != nil {
+ return apiError(currentRetrieval, err)
+ }
+
+ res := response.Status{
+ CurrentBlockID: current,
+ CurrentBlockTimestamp: timestamp.UnixNano() / 1_000_000,
+ OldestBlockID: oldest,
+ GenesisBlockID: oldest,
+ Peers: []struct{}{},
+ }
+
+ return ctx.JSON(statusOK, res)
+}
diff --git a/api/rosetta/status_codes.go b/api/rosetta/status_codes.go
new file mode 100755
index 0000000..fc7f79f
--- /dev/null
+++ b/api/rosetta/status_codes.go
@@ -0,0 +1,41 @@
+// Copyright 2021 Optakt Labs OÜ
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+package rosetta
+
+import (
+ "net/http"
+)
+
+// The Rosetta API specification expects every error returned from the Rosetta
+// API to be a HTTP status code 500 (internal server error). We optionally make
+// it possible to have a more expressive API by returning meaningful HTTP status
+// codes where appropriate.
+var (
+ statusOK = http.StatusOK
+ statusBadRequest = http.StatusInternalServerError
+ statusUnprocessableEntity = http.StatusInternalServerError
+ statusInternalServerError = http.StatusInternalServerError
+)
+
+// EnableSmartCodes overwrites the global variables that determine which error
+// codes the Rosetta API returns. While we avoid global variables in general,
+// these function more as a proxy to the constants of the HTTP package, with the
+// ability to change their value.
+func EnableSmartCodes() {
+ statusOK = http.StatusOK
+ statusBadRequest = http.StatusBadRequest
+ statusUnprocessableEntity = http.StatusUnprocessableEntity
+ statusInternalServerError = http.StatusInternalServerError
+}
diff --git a/api/rosetta/status_integration_test.go b/api/rosetta/status_integration_test.go
new file mode 100755
index 0000000..deba28e
--- /dev/null
+++ b/api/rosetta/status_integration_test.go
@@ -0,0 +1,247 @@
+// Copyright 2021 Optakt Labs OÜ
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+//go:build integration
+// +build integration
+
+package rosetta_test
+
+import (
+ "encoding/json"
+ "net/http"
+ "testing"
+
+ "github.com/labstack/echo/v4"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+
+ "github.com/optakt/flow-dps/models/convert"
+ "github.com/optakt/flow-dps/models/dps"
+ "github.com/optakt/flow-rosetta/api/rosetta"
+ "github.com/optakt/flow-rosetta/rosetta/configuration"
+ "github.com/optakt/flow-rosetta/rosetta/identifier"
+ "github.com/optakt/flow-rosetta/rosetta/request"
+ "github.com/optakt/flow-rosetta/rosetta/response"
+)
+
+func TestAPI_Status(t *testing.T) {
+ // TODO: Repair integration tests
+ // See https://github.com/optakt/flow-dps/issues/333
+ t.Skip("integration tests disabled until new snapshot is generated")
+
+ db := setupDB(t)
+ api := setupAPI(t, db)
+
+ oldestBlockID := knownHeader(0).ID().String()
+ lastBlock := knownHeader(425)
+
+ request := request.Status{
+ NetworkID: defaultNetwork(),
+ }
+
+ rec, ctx, err := setupRecorder(statusEndpoint, request)
+ require.NoError(t, err)
+
+ err = api.Status(ctx)
+ assert.NoError(t, err)
+
+ assert.Equal(t, http.StatusOK, rec.Result().StatusCode)
+
+ var status response.Status
+ require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &status))
+
+ currentHeight := status.CurrentBlockID.Index
+ require.NotNil(t, currentHeight)
+ assert.Equal(t, *currentHeight, lastBlock.Height)
+
+ assert.Equal(t, status.CurrentBlockID.Hash, lastBlock.ID().String())
+ assert.Equal(t, status.CurrentBlockTimestamp, convert.RosettaTime(lastBlock.Timestamp))
+
+ assert.Equal(t, status.OldestBlockID.Hash, oldestBlockID)
+
+ oldestHeight := status.OldestBlockID.Index
+ require.NotNil(t, oldestHeight)
+ assert.Equal(t, *oldestHeight, uint64(0))
+
+ assert.Equal(t, status.GenesisBlockID.Hash, oldestBlockID)
+
+ genesisBlockHeight := status.GenesisBlockID.Index
+ require.NotNil(t, genesisBlockHeight)
+ assert.Equal(t, *genesisBlockHeight, uint64(0))
+}
+
+func TestAPI_StatusHandlesErrors(t *testing.T) {
+ // TODO: Repair integration tests
+ // See https://github.com/optakt/flow-dps/issues/333
+ t.Skip("integration tests disabled until new snapshot is generated")
+
+ db := setupDB(t)
+ api := setupAPI(t, db)
+
+ tests := []struct {
+ name string
+
+ request request.Status
+
+ checkError assert.ErrorAssertionFunc
+ }{
+ {
+ name: "missing blockchain",
+ request: request.Status{
+ NetworkID: identifier.Network{
+ Blockchain: "",
+ Network: dps.FlowTestnet.String(),
+ },
+ },
+
+ checkError: checkRosettaError(http.StatusBadRequest, configuration.ErrorInvalidFormat),
+ },
+ {
+ name: "invalid blockchain",
+ request: request.Status{
+ NetworkID: identifier.Network{
+ Blockchain: invalidBlockchain,
+ Network: dps.FlowTestnet.String(),
+ },
+ },
+
+ checkError: checkRosettaError(http.StatusUnprocessableEntity, configuration.ErrorInvalidNetwork),
+ },
+ {
+ name: "missing network",
+ request: request.Status{
+ NetworkID: identifier.Network{
+ Blockchain: dps.FlowBlockchain,
+ Network: "",
+ },
+ },
+
+ checkError: checkRosettaError(http.StatusBadRequest, configuration.ErrorInvalidFormat),
+ },
+ {
+ name: "invalid network",
+ request: request.Status{
+ NetworkID: identifier.Network{
+ Blockchain: dps.FlowBlockchain,
+ Network: invalidNetwork,
+ },
+ },
+
+ checkError: checkRosettaError(http.StatusUnprocessableEntity, configuration.ErrorInvalidNetwork),
+ },
+ }
+
+ for _, test := range tests {
+
+ test := test
+ t.Run(test.name, func(t *testing.T) {
+
+ t.Parallel()
+
+ _, ctx, err := setupRecorder(statusEndpoint, test.request)
+ require.NoError(t, err)
+
+ err = api.Status(ctx)
+ test.checkError(t, err)
+ })
+ }
+}
+
+func TestAPI_StatusHandlerMalformedRequest(t *testing.T) {
+ // TODO: Repair integration tests
+ // See https://github.com/optakt/flow-dps/issues/333
+ t.Skip("integration tests disabled until new snapshot is generated")
+
+ db := setupDB(t)
+ api := setupAPI(t, db)
+
+ const (
+ // wrong type for 'network' field
+ wrongFieldType = `{
+ "network_identifier": {
+ "blockchain": "flow",
+ "network": 99
+ }
+ }`
+
+ // malformed JSON - unclosed bracket
+ unclosedBracket = `{
+ "network_identifier": {
+ "blockchain": "flow",
+ "network": "flow-testnet"
+ }`
+
+ validPayload = `{
+ "network_identifier": {
+ "blockchain": "flow",
+ "network": "flow-testnet"
+ }
+ }`
+ )
+
+ tests := []struct {
+ name string
+
+ payload []byte
+
+ prepare func(*http.Request)
+ }{
+ {
+ name: "invalid status input types",
+ payload: []byte(wrongFieldType),
+ prepare: func(req *http.Request) {
+ req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON)
+ },
+ },
+ {
+ name: "invalid status json format",
+ payload: []byte(unclosedBracket),
+ prepare: func(req *http.Request) {
+ req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON)
+ },
+ },
+ {
+ name: "valid status payload with no MIME type",
+ payload: []byte(validPayload),
+ prepare: func(req *http.Request) {
+ req.Header.Set(echo.HeaderContentType, "")
+ },
+ },
+ }
+
+ for _, test := range tests {
+
+ test := test
+ t.Run(test.name, func(t *testing.T) {
+
+ t.Parallel()
+
+ _, ctx, err := setupRecorder(statusEndpoint, test.payload, test.prepare)
+ require.NoError(t, err)
+
+ // execute the request
+ err = api.Status(ctx)
+ assert.Error(t, err)
+
+ echoErr, ok := err.(*echo.HTTPError)
+ require.True(t, ok)
+
+ assert.Equal(t, http.StatusBadRequest, echoErr.Code)
+ gotErr, ok := echoErr.Message.(rosetta.Error)
+ require.True(t, ok)
+
+ assert.Equal(t, configuration.ErrorInvalidEncoding, gotErr.ErrorDefinition)
+ })
+ }
+}
diff --git a/api/rosetta/submit.go b/api/rosetta/submit.go
new file mode 100755
index 0000000..5334542
--- /dev/null
+++ b/api/rosetta/submit.go
@@ -0,0 +1,51 @@
+// Copyright 2021 Optakt Labs OÜ
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+package rosetta
+
+import (
+ "github.com/labstack/echo/v4"
+
+ "github.com/optakt/flow-rosetta/rosetta/request"
+ "github.com/optakt/flow-rosetta/rosetta/response"
+)
+
+// Submit implements the /construction/submit endpoint of the Rosetta Construction API.
+// Submit endpoint receives the fully constructed, signed transaction and submits it
+// for execution to the Flow network using the SendTransaction API call of the Flow Access API.
+// See https://www.rosetta-api.org/docs/ConstructionApi.html#constructionsubmit
+func (c *Construction) Submit(ctx echo.Context) error {
+
+ var req request.Submit
+ err := ctx.Bind(&req)
+ if err != nil {
+ return unpackError(err)
+ }
+
+ err = c.validate.Request(req)
+ if err != nil {
+ return formatError(err)
+ }
+
+ rosTxID, err := c.transact.SubmitTransaction(req.SignedTransaction)
+ if err != nil {
+ return apiError(txSubmission, err)
+ }
+
+ res := response.Submit{
+ TransactionID: rosTxID,
+ }
+
+ return ctx.JSON(statusOK, res)
+}
diff --git a/api/rosetta/transaction.go b/api/rosetta/transaction.go
new file mode 100755
index 0000000..cae148e
--- /dev/null
+++ b/api/rosetta/transaction.go
@@ -0,0 +1,54 @@
+// Copyright 2021 Optakt Labs OÜ
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+package rosetta
+
+import (
+ "github.com/labstack/echo/v4"
+
+ "github.com/optakt/flow-rosetta/rosetta/request"
+ "github.com/optakt/flow-rosetta/rosetta/response"
+)
+
+// Transaction implements the /block/transaction endpoint of the Rosetta Data API.
+// See https://www.rosetta-api.org/docs/BlockApi.html#blocktransaction
+func (d *Data) Transaction(ctx echo.Context) error {
+
+ var req request.Transaction
+ err := ctx.Bind(&req)
+ if err != nil {
+ return unpackError(err)
+ }
+
+ err = d.validate.Request(req)
+ if err != nil {
+ return formatError(err)
+ }
+
+ err = d.validate.CompleteBlockID(req.BlockID)
+ if err != nil {
+ return formatError(err)
+ }
+
+ transaction, err := d.retrieve.Transaction(req.BlockID, req.TransactionID)
+ if err != nil {
+ return apiError(txRetrieval, err)
+ }
+
+ res := response.Transaction{
+ Transaction: transaction,
+ }
+
+ return ctx.JSON(statusOK, res)
+}
diff --git a/api/rosetta/transaction_integration_test.go b/api/rosetta/transaction_integration_test.go
new file mode 100755
index 0000000..3233f7c
--- /dev/null
+++ b/api/rosetta/transaction_integration_test.go
@@ -0,0 +1,470 @@
+// Copyright 2021 Optakt Labs OÜ
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+//go:build integration
+// +build integration
+
+package rosetta_test
+
+import (
+ "encoding/json"
+ "net/http"
+ "testing"
+
+ "github.com/labstack/echo/v4"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+
+ "github.com/onflow/flow-go/model/flow"
+
+ "github.com/optakt/flow-dps/models/dps"
+ "github.com/optakt/flow-rosetta/api/rosetta"
+ "github.com/optakt/flow-rosetta/rosetta/configuration"
+ "github.com/optakt/flow-rosetta/rosetta/identifier"
+ "github.com/optakt/flow-rosetta/rosetta/object"
+ "github.com/optakt/flow-rosetta/rosetta/request"
+ "github.com/optakt/flow-rosetta/rosetta/response"
+ "github.com/optakt/flow-rosetta/testing/mocks"
+)
+
+func TestAPI_Transaction(t *testing.T) {
+ // TODO: Repair integration tests
+ // See https://github.com/optakt/flow-dps/issues/333
+ t.Skip("integration tests disabled until new snapshot is generated")
+
+ db := setupDB(t)
+ api := setupAPI(t, db)
+
+ var (
+ firstHeader = knownHeader(44)
+ multipleTxHeader = knownHeader(165)
+ lastHeader = knownHeader(181)
+
+ // two transactions in a single block
+ midBlockTxs = []string{
+ "23c486cfd54bca7138b519203322327bf46e43a780a237d1c5bb0a82f0a06c1d",
+ "3d6922d6c6fd161a76cec23b11067f22cac6409a49b28b905989db64f5cb05a5",
+ }
+ )
+
+ const (
+ firstTx = "d5c18baf6c8d11f0693e71dbb951c4856d4f25a456f4d5285a75fd73af39161c"
+ lastTx = "780bafaf4721ca4270986ea51e659951a8912c2eb99fb1bfedeb753b023cd4d9"
+ )
+
+ tests := []struct {
+ name string
+
+ request request.Transaction
+ validateTx validateTxFunc
+ }{
+ {
+ name: "some cherry picked transaction",
+ request: requestTransaction(firstHeader, firstTx),
+ validateTx: validateTransfer(t, firstTx, "754aed9de6197641", "631e88ae7f1d7c20", 1),
+ },
+ {
+ name: "first in a block with multiple",
+ request: requestTransaction(multipleTxHeader, midBlockTxs[0]),
+ validateTx: validateTransfer(t, midBlockTxs[0], "8c5303eaa26202d6", "72157877737ce077", 100_00000000),
+ },
+ {
+ // The test does not have blocks with more than two transactions, so this is the same as 'get the last transaction from a block'.
+ name: "second in a block with multiple",
+ request: requestTransaction(multipleTxHeader, midBlockTxs[1]),
+ validateTx: validateTransfer(t, midBlockTxs[1], "89c61aa64423504c", "82ec283f88a62e65", 1),
+ },
+ {
+ name: "last transaction recorded",
+ request: requestTransaction(lastHeader, lastTx),
+ validateTx: validateTransfer(t, lastTx, "668b91e2995c2eba", "89c61aa64423504c", 1),
+ },
+ }
+
+ for _, test := range tests {
+
+ test := test
+ t.Run(test.name, func(t *testing.T) {
+
+ t.Parallel()
+
+ rec, ctx, err := setupRecorder(transactionEndpoint, test.request)
+ require.NoError(t, err)
+
+ err = api.Transaction(ctx)
+ assert.NoError(t, err)
+
+ assert.Equal(t, http.StatusOK, rec.Result().StatusCode)
+
+ var res response.Transaction
+ require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &res))
+
+ test.validateTx([]*object.Transaction{res.Transaction})
+ })
+ }
+}
+
+func TestAPI_TransactionHandlesErrors(t *testing.T) {
+ // TODO: Repair integration tests
+ // See https://github.com/optakt/flow-dps/issues/333
+ t.Skip("integration tests disabled until new snapshot is generated")
+
+ db := setupDB(t)
+ api := setupAPI(t, db)
+
+ var testHeight uint64 = 106
+
+ const (
+ lastHeight = 425
+
+ testBlockHash = "1f269f0f45cd2e368e82902d96247113b74da86f6205adf1fd8cf2365418d275"
+ testTxHash = "071e5810f1c8c934aec260f7847400af8f77607ed27ecc02668d7bb2c287c683"
+
+ trimmedBlockHash = "1f269f0f45cd2e368e82902d96247113b74da86f6205adf1fd8cf2365418d27" // block hash a character short
+ trimmedTxHash = "071e5810f1c8c934aec260f7847400af8f77607ed27ecc02668d7bb2c287c68" // tx hash a character short
+ invalidTxHash = "071e5810f1c8c934aec260f7847400af8f77607ed27ecc02668d7bb2c287c68z" // testTxHash with a hex-invalid last character
+ unknownTxHash = "602dd6b7fad80b0e6869eaafd55625faa16341f09027dc925a8e8cef267e5683" // tx from another block
+ )
+
+ var (
+ testBlock = identifier.Block{
+ Index: &testHeight,
+ Hash: testBlockHash,
+ }
+
+ // corresponds to the block above
+ testTx = identifier.Transaction{Hash: testTxHash}
+ )
+
+ tests := []struct {
+ name string
+
+ request request.Transaction
+
+ checkErr assert.ErrorAssertionFunc
+ }{
+ {
+ name: "empty transaction request",
+ request: request.Transaction{},
+
+ checkErr: checkRosettaError(http.StatusBadRequest, configuration.ErrorInvalidFormat),
+ },
+ {
+ name: "missing blockchain name",
+ request: request.Transaction{
+ NetworkID: identifier.Network{
+ Blockchain: "",
+ Network: dps.FlowTestnet.String(),
+ },
+ BlockID: testBlock,
+ TransactionID: testTx,
+ },
+
+ checkErr: checkRosettaError(http.StatusBadRequest, configuration.ErrorInvalidFormat),
+ },
+ {
+ name: "invalid blockchain name",
+ request: request.Transaction{
+ NetworkID: identifier.Network{
+ Blockchain: invalidBlockchain,
+ Network: dps.FlowTestnet.String(),
+ },
+ BlockID: testBlock,
+ TransactionID: testTx,
+ },
+
+ checkErr: checkRosettaError(http.StatusUnprocessableEntity, configuration.ErrorInvalidNetwork),
+ },
+ {
+ name: "missing network name",
+ request: request.Transaction{
+ NetworkID: identifier.Network{
+ Blockchain: dps.FlowBlockchain,
+ Network: "",
+ },
+ BlockID: testBlock,
+ TransactionID: testTx,
+ },
+
+ checkErr: checkRosettaError(http.StatusBadRequest, configuration.ErrorInvalidFormat),
+ },
+ {
+ name: "invalid network name",
+ request: request.Transaction{
+ NetworkID: identifier.Network{
+ Blockchain: dps.FlowBlockchain,
+ Network: invalidNetwork,
+ },
+ BlockID: testBlock,
+ TransactionID: testTx,
+ },
+
+ checkErr: checkRosettaError(http.StatusUnprocessableEntity, configuration.ErrorInvalidNetwork),
+ },
+ {
+ name: "missing block height and hash",
+ request: request.Transaction{
+ NetworkID: defaultNetwork(),
+ BlockID: identifier.Block{
+ Index: nil,
+ Hash: "",
+ },
+ TransactionID: testTx,
+ },
+
+ checkErr: checkRosettaError(http.StatusBadRequest, configuration.ErrorInvalidFormat),
+ },
+ {
+ name: "invalid length of block id",
+ request: request.Transaction{
+ NetworkID: defaultNetwork(),
+ BlockID: identifier.Block{
+ Index: &testHeight,
+ Hash: trimmedBlockHash,
+ },
+ TransactionID: testTx,
+ },
+
+ checkErr: checkRosettaError(http.StatusBadRequest, configuration.ErrorInvalidFormat),
+ },
+ {
+ name: "missing block height",
+ request: request.Transaction{
+ NetworkID: defaultNetwork(),
+ BlockID: identifier.Block{
+ Hash: testBlockHash,
+ },
+ TransactionID: testTx,
+ },
+ checkErr: checkRosettaError(http.StatusBadRequest, configuration.ErrorInvalidFormat),
+ },
+ {
+ name: "invalid block hash",
+ request: request.Transaction{
+ NetworkID: defaultNetwork(),
+ BlockID: identifier.Block{
+ Index: &testHeight,
+ Hash: invalidBlockHash,
+ },
+ TransactionID: testTx,
+ },
+
+ checkErr: checkRosettaError(http.StatusUnprocessableEntity, configuration.ErrorInvalidBlock),
+ },
+ {
+ name: "unknown block",
+ request: request.Transaction{
+ NetworkID: defaultNetwork(),
+ BlockID: identifier.Block{
+ Index: getUint64P(lastHeight + 1),
+ Hash: mocks.GenericRosBlockID.Hash,
+ },
+ TransactionID: testTx,
+ },
+
+ checkErr: checkRosettaError(http.StatusUnprocessableEntity, configuration.ErrorUnknownBlock),
+ },
+ {
+ name: "mismatched block height and hash",
+ request: request.Transaction{
+ NetworkID: defaultNetwork(),
+ BlockID: identifier.Block{
+ Index: getUint64P(44),
+ Hash: testBlockHash,
+ },
+ TransactionID: testTx,
+ },
+
+ checkErr: checkRosettaError(http.StatusUnprocessableEntity, configuration.ErrorInvalidBlock),
+ },
+ {
+ name: "missing transaction id",
+ request: request.Transaction{
+ NetworkID: defaultNetwork(),
+ BlockID: testBlock,
+ TransactionID: identifier.Transaction{
+ Hash: "",
+ },
+ },
+
+ checkErr: checkRosettaError(http.StatusBadRequest, configuration.ErrorInvalidFormat),
+ },
+ {
+ name: "missing transaction id",
+ request: request.Transaction{
+ NetworkID: defaultNetwork(),
+ BlockID: testBlock,
+ TransactionID: identifier.Transaction{
+ Hash: trimmedTxHash,
+ },
+ },
+
+ checkErr: checkRosettaError(http.StatusBadRequest, configuration.ErrorInvalidFormat),
+ },
+ {
+ name: "invalid transaction id",
+ request: request.Transaction{
+ NetworkID: defaultNetwork(),
+ BlockID: testBlock,
+ TransactionID: identifier.Transaction{
+ Hash: invalidTxHash,
+ },
+ },
+
+ checkErr: checkRosettaError(http.StatusUnprocessableEntity, configuration.ErrorInvalidTransaction),
+ },
+ // TODO: Add test case for transaction with no events/transfers.
+ // See https://github.com/optakt/flow-dps/issues/452
+ {
+ name: "transaction missing from block",
+ request: request.Transaction{
+ NetworkID: defaultNetwork(),
+ TransactionID: identifier.Transaction{
+ Hash: unknownTxHash,
+ },
+ BlockID: testBlock,
+ },
+
+ checkErr: checkRosettaError(http.StatusUnprocessableEntity, configuration.ErrorUnknownTransaction),
+ },
+ }
+
+ for _, test := range tests {
+ test := test
+ t.Run(test.name, func(t *testing.T) {
+ t.Parallel()
+
+ _, ctx, err := setupRecorder(transactionEndpoint, test.request)
+ require.NoError(t, err)
+
+ err = api.Transaction(ctx)
+ test.checkErr(t, err)
+ })
+ }
+}
+
+func TestAPI_TransactionHandlesMalformedRequest(t *testing.T) {
+ // TODO: Repair integration tests
+ // See https://github.com/optakt/flow-dps/issues/333
+ t.Skip("integration tests disabled until new snapshot is generated")
+
+ db := setupDB(t)
+ api := setupAPI(t, db)
+
+ const (
+ // network field is an integer instead of a string
+ wrongFieldType = `
+ {
+ "network_identifier": {
+ "blockchain": "flow",
+ "network": 99
+ }
+ }`
+
+ unclosedBracket = `
+ {
+ "network_identifier" : {
+ "blockchain": "flow",
+ "network": "flow-testnet"
+ },
+ "block_identifier": {
+ "index": 106,
+ "hash": "1f269f0f45cd2e368e82902d96247113b74da86f6205adf1fd8cf2365418d275"
+ },
+ "transaction_identifier": {
+ "hash": "071e5810f1c8c934aec260f7847400af8f77607ed27ecc02668d7bb2c287c683"
+ }`
+
+ validJSON = `
+ {
+ "network_identifier" : {
+ "blockchain": "flow",
+ "network": "flow-testnet"
+ },
+ "block_identifier": {
+ "index": 106,
+ "hash": "1f269f0f45cd2e368e82902d96247113b74da86f6205adf1fd8cf2365418d275"
+ },
+ "transaction_identifier": {
+ "hash": "071e5810f1c8c934aec260f7847400af8f77607ed27ecc02668d7bb2c287c683"
+ }
+ }`
+ )
+
+ tests := []struct {
+ name string
+ payload []byte
+ prepare func(*http.Request)
+ }{
+ {
+ name: "wrong field type",
+ payload: []byte(wrongFieldType),
+ prepare: func(req *http.Request) {
+ req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON)
+ },
+ },
+ {
+ name: "unclosed bracket",
+ payload: []byte(unclosedBracket),
+ prepare: func(req *http.Request) {
+ req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON)
+ },
+ },
+ {
+ name: "valid payload with no MIME type set",
+ payload: []byte(validJSON),
+ prepare: func(req *http.Request) {
+ req.Header.Set(echo.HeaderContentType, "")
+ },
+ },
+ }
+
+ for _, test := range tests {
+ test := test
+ t.Run(test.name, func(t *testing.T) {
+ t.Parallel()
+
+ _, ctx, err := setupRecorder(transactionEndpoint, test.payload, test.prepare)
+ require.NoError(t, err)
+
+ err = api.Block(ctx)
+ assert.Error(t, err)
+
+ echoErr, ok := err.(*echo.HTTPError)
+ require.True(t, ok)
+
+ assert.Equal(t, http.StatusBadRequest, echoErr.Code)
+
+ gotErr, ok := echoErr.Message.(rosetta.Error)
+ require.True(t, ok)
+
+ assert.Equal(t, configuration.ErrorInvalidEncoding, gotErr.ErrorDefinition)
+ assert.NotEmpty(t, gotErr.Description)
+ })
+ }
+
+}
+
+func requestTransaction(header flow.Header, txID string) request.Transaction {
+ return request.Transaction{
+ NetworkID: defaultNetwork(),
+ BlockID: identifier.Block{
+ Index: &header.Height,
+ Hash: header.ID().String(),
+ },
+ TransactionID: identifier.Transaction{
+ Hash: txID,
+ },
+ }
+}
diff --git a/api/rosetta/transactor.go b/api/rosetta/transactor.go
new file mode 100755
index 0000000..7762e9a
--- /dev/null
+++ b/api/rosetta/transactor.go
@@ -0,0 +1,32 @@
+// Copyright 2021 Optakt Labs OÜ
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+package rosetta
+
+import (
+ "github.com/optakt/flow-rosetta/rosetta/identifier"
+ "github.com/optakt/flow-rosetta/rosetta/object"
+ "github.com/optakt/flow-rosetta/rosetta/transactor"
+)
+
+// Transactor is used by the Rosetta Construction API to handle transaction related operations.
+type Transactor interface {
+ DeriveIntent(operations []object.Operation) (intent *transactor.Intent, err error)
+ CompileTransaction(refBlockID identifier.Block, intent *transactor.Intent, sequence uint64) (unsigned string, err error)
+ HashPayload(rosBlockID identifier.Block, unsigned string, signer identifier.Account) (algo string, hash string, err error)
+ Parse(payload string) (transactor.Parser, error)
+ AttachSignatures(unsigned string, signatures []object.Signature) (signed string, err error)
+ TransactionIdentifier(signed string) (rosTxID identifier.Transaction, err error)
+ SubmitTransaction(signed string) (rosTxID identifier.Transaction, err error)
+}
diff --git a/api/rosetta/validator.go b/api/rosetta/validator.go
new file mode 100755
index 0000000..d4c9bcc
--- /dev/null
+++ b/api/rosetta/validator.go
@@ -0,0 +1,24 @@
+// Copyright 2021 Optakt Labs OÜ
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+package rosetta
+
+import (
+ "github.com/optakt/flow-rosetta/rosetta/identifier"
+)
+
+type Validator interface {
+ Request(interface{}) error
+ CompleteBlockID(identifier.Block) error
+}
diff --git a/docs/rosetta-api.md b/docs/rosetta-api.md
new file mode 100755
index 0000000..808286d
--- /dev/null
+++ b/docs/rosetta-api.md
@@ -0,0 +1,29 @@
+# Rosetta API
+
+The Rosetta API needs its own documentation because of the amount of components it has that interact with each other.
+The main reason for its complexity is that it needs to interact with the Flow Virtual Machine (FVM) and to translate between the Flow and Rosetta application domains.
+
+## Invoker
+
+This component, given a Cadence script, can execute it at any given height and return the value produced by the script.
+
+[Package documentation](https://pkg.go.dev/github.com/optakt/flow-rosetta/rosetta/invoker)
+
+## Retriever
+
+The retriever uses the other components to retrieve account balances, blocks and transactions.
+
+[Package documentation](https://pkg.go.dev/github.com/optakt/flow-rosetta/rosetta/retriever)
+
+## Scripts
+
+The script package produces Cadence scripts with the correct imports and storage paths, depending on the configured Flow chain ID.
+
+[Package documentation](https://pkg.go.dev/github.com/optakt/flow-rosetta/rosetta/scripts)
+
+## Validator
+
+The Validator component validates whether the given Rosetta identifiers are valid.
+
+[Package documentation](https://pkg.go.dev/github.com/optakt/flow-rosetta/rosetta/validator)
+
diff --git a/gen/version.go b/gen/version.go
new file mode 100755
index 0000000..2c54d2f
--- /dev/null
+++ b/gen/version.go
@@ -0,0 +1,149 @@
+// Copyright 2021 Optakt Labs OÜ
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+//go:generate go run version.go
+
+package main
+
+import (
+ "fmt"
+ "log"
+ "os"
+ "strings"
+ "text/template"
+
+ "golang.org/x/mod/modfile"
+)
+
+const versionFileTemplate = `// Copyright 2021 Optakt Labs OÜ
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+package configuration
+
+const (
+ RosettaVersion = "{{ .RosettaVersion }}"
+ NodeVersion = "{{ .NodeVersion }}"
+ MiddlewareVersion = "{{ .MiddlewareVersion }}"
+)
+`
+
+const (
+ rosettaVersion = "1.4.10"
+
+ pathToGoMod = "../go.mod"
+ rosettaVersionFilePath = "../rosetta/configuration/version.go"
+
+ flowModPath = "github.com/onflow/flow-go"
+ dpsModPath = "github.com/optakt/flow-dps"
+)
+
+func main() {
+
+ fmt.Println("Using rosetta version", rosettaVersion)
+
+ nodeVersion, err := NodeVersion()
+ if err != nil {
+ log.Fatalf("could not compute node version: %v", err)
+ }
+
+ fmt.Println("Found node version", nodeVersion)
+
+ middlewareVersion, err := MiddlewareVersion()
+ if err != nil {
+ log.Fatalf("could not compute middleware version: %v", err)
+ }
+
+ fmt.Println("Found middleware version", middlewareVersion)
+
+ tmpl := template.Must(template.New("version.go").Parse(versionFileTemplate))
+
+ versionFile, err := os.Create(rosettaVersionFilePath)
+ if err != nil {
+ log.Fatalf("could not open version file: %v", err)
+ }
+
+ args := struct {
+ RosettaVersion string
+ NodeVersion string
+ MiddlewareVersion string
+ }{
+ RosettaVersion: rosettaVersion,
+ NodeVersion: nodeVersion,
+ MiddlewareVersion: middlewareVersion,
+ }
+
+ err = tmpl.Execute(versionFile, args)
+ if err != nil {
+ log.Fatalf("could not execute template: %v", err)
+ }
+}
+
+// MiddlewareVersion parses the Go.mod file to retrieve the version of the Flow-DPS
+// dependency.
+func MiddlewareVersion() (string, error) {
+ gomod, err := os.ReadFile(pathToGoMod)
+ if err != nil {
+ return "", fmt.Errorf("could not read go mod file: %w", err)
+ }
+
+ modfile, err := modfile.Parse("go.mod", gomod, nil)
+ if err != nil {
+ return "", fmt.Errorf("could not parse go mod file: %w", err)
+ }
+
+ for _, module := range modfile.Require {
+ if module.Mod.Path == dpsModPath {
+ // Strip leading `v` from the tag if it exists.
+ nodeVersion := strings.TrimPrefix(module.Mod.Version, "v")
+ return nodeVersion, nil
+ }
+ }
+
+ return "", fmt.Errorf("could not find github.com/optakt/flow-dps dependency in go mod file")
+}
+
+// NodeVersion parses the Go.mod file to retrieve the version of the Flow-go
+// dependency.
+func NodeVersion() (string, error) {
+ gomod, err := os.ReadFile(pathToGoMod)
+ if err != nil {
+ return "", fmt.Errorf("could not read go mod file: %w", err)
+ }
+
+ modfile, err := modfile.Parse("go.mod", gomod, nil)
+ if err != nil {
+ return "", fmt.Errorf("could not parse go mod file: %w", err)
+ }
+
+ for _, module := range modfile.Require {
+ if module.Mod.Path == flowModPath {
+ // Strip leading `v` from the tag if it exists.
+ nodeVersion := strings.TrimPrefix(module.Mod.Version, "v")
+ return nodeVersion, nil
+ }
+ }
+
+ return "", fmt.Errorf("could not find github.com/onflow/flow-go dependency in go mod file")
+}
diff --git a/go.mod b/go.mod
new file mode 100755
index 0000000..45419e4
--- /dev/null
+++ b/go.mod
@@ -0,0 +1,19 @@
+module github.com/optakt/flow-rosetta
+
+go 1.17
+
+require (
+ github.com/dgraph-io/badger/v2 v2.2007.4
+ github.com/go-playground/validator/v10 v10.9.0
+ github.com/klauspost/compress v1.13.5
+ github.com/labstack/echo/v4 v4.5.0
+ github.com/onflow/cadence v0.19.1
+ github.com/onflow/flow-go v0.21.2
+ github.com/onflow/flow-go-sdk v0.21.0
+ github.com/rs/zerolog v1.25.0
+ github.com/spf13/pflag v1.0.5
+ github.com/stretchr/testify v1.7.0
+ github.com/ziflex/lecho/v2 v2.5.1
+ golang.org/x/mod v0.5.0
+ google.golang.org/grpc v1.40.0
+)
\ No newline at end of file
diff --git a/go.sum b/go.sum
new file mode 100644
index 0000000..60a4ee7
--- /dev/null
+++ b/go.sum
@@ -0,0 +1,4 @@
+github.com/dgraph-io/badger/v2 v2.2007.4 h1:TRWBQg8UrlUhaFdco01nO2uXwzKS7zd+HVdwV/GHc4o=
+github.com/dgraph-io/badger/v2 v2.2007.4/go.mod h1:vSw/ax2qojzbN6eXHIx6KPKtCSHJN/Uz0X0VPruTIhk=
+golang.org/x/mod v0.5.0 h1:UG21uOlmZabA4fW5i7ZX6bjw1xELEGg/ZLgZq9auk/Q=
+golang.org/x/mod v0.5.0/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
diff --git a/models/convert/convert/cadence.go b/models/convert/convert/cadence.go
new file mode 100755
index 0000000..65f909d
--- /dev/null
+++ b/models/convert/convert/cadence.go
@@ -0,0 +1,181 @@
+// Copyright 2021 Optakt Labs OÜ
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+package convert
+
+import (
+ "encoding/hex"
+ "fmt"
+ "math/big"
+ "regexp"
+ "strconv"
+
+ "github.com/onflow/cadence"
+)
+
+// ParseCadenceArgument parses strings that contain Cadence parameters into cadence values.
+func ParseCadenceArgument(param string) (cadence.Value, error) {
+
+ // Cadence values should be provided in the form of Type(Value), so that we
+ // can unambiguously determine the type.
+ re := regexp.MustCompile(`(\w+)\((.+)\)`)
+ parts := re.FindStringSubmatch(param)
+ if len(parts) != 3 {
+ return nil, fmt.Errorf("invalid parameter format (%s)", param)
+ }
+ typ := parts[1]
+ val := parts[2]
+
+ // Now, we can switch on the type and parse accordingly.
+ switch typ {
+ case "Bool":
+ b, err := strconv.ParseBool(val)
+ if err != nil {
+ return nil, fmt.Errorf("could not parse boolean: %w", err)
+ }
+ return cadence.NewBool(b), nil
+
+ case "Int":
+ v, err := strconv.ParseInt(val, 10, 0)
+ if err != nil {
+ return nil, fmt.Errorf("could not parse integer: %w", err)
+ }
+ return cadence.NewInt(int(v)), nil
+
+ case "Int8":
+ v, err := strconv.ParseInt(val, 10, 8)
+ if err != nil {
+ return nil, fmt.Errorf("could not parse integer: %w", err)
+ }
+ return cadence.NewInt8(int8(v)), nil
+
+ case "Int16":
+ v, err := strconv.ParseInt(val, 10, 16)
+ if err != nil {
+ return nil, fmt.Errorf("could not parse integer: %w", err)
+ }
+ return cadence.NewInt16(int16(v)), nil
+
+ case "Int32":
+ v, err := strconv.ParseInt(val, 10, 32)
+ if err != nil {
+ return nil, fmt.Errorf("could not parse integer: %w", err)
+ }
+ return cadence.NewInt32(int32(v)), nil
+
+ case "Int64":
+ v, err := strconv.ParseInt(val, 10, 64)
+ if err != nil {
+ return nil, fmt.Errorf("could not parse integer: %w", err)
+ }
+ return cadence.NewInt64(v), nil
+
+ case "Int128":
+ v, ok := big.NewInt(0).SetString(val, 10)
+ if !ok {
+ return nil, fmt.Errorf("could not parse big integer (%s)", val)
+ }
+ return cadence.NewInt128FromBig(v)
+
+ case "Int256":
+ v, ok := big.NewInt(0).SetString(val, 10)
+ if !ok {
+ return nil, fmt.Errorf("could not parse big integer (%s)", val)
+ }
+ return cadence.NewInt256FromBig(v)
+
+ case "UInt":
+ v, err := strconv.ParseUint(val, 10, 0)
+ if err != nil {
+ return nil, fmt.Errorf("could not parse unsigned integer: %w", err)
+ }
+ return cadence.NewUInt(uint(v)), nil
+
+ case "UInt8":
+ v, err := strconv.ParseUint(val, 10, 8)
+ if err != nil {
+ return nil, fmt.Errorf("could not parse unsigned integer: %w", err)
+ }
+ return cadence.NewUInt8(uint8(v)), nil
+
+ case "UInt16":
+ v, err := strconv.ParseUint(val, 10, 16)
+ if err != nil {
+ return nil, fmt.Errorf("could not parse unsigned integer: %w", err)
+ }
+ return cadence.NewUInt16(uint16(v)), nil
+
+ case "UInt32":
+ v, err := strconv.ParseUint(val, 10, 32)
+ if err != nil {
+ return nil, fmt.Errorf("could not parse integer: %w", err)
+ }
+ return cadence.NewUInt32(uint32(v)), nil
+
+ case "UInt64":
+ v, err := strconv.ParseUint(val, 10, 64)
+ if err != nil {
+ return nil, fmt.Errorf("could not parse unsigned integer: %w", err)
+ }
+ return cadence.NewUInt64(v), nil
+
+ case "UInt128":
+ v, ok := big.NewInt(0).SetString(val, 10)
+ if !ok {
+ return nil, fmt.Errorf("could not parse big integer (%s)", val)
+ }
+ return cadence.NewUInt128FromBig(v)
+
+ case "UInt256":
+ v, ok := big.NewInt(0).SetString(val, 10)
+ if !ok {
+ return nil, fmt.Errorf("could not parse big integer (%s)", val)
+ }
+ return cadence.NewUInt256FromBig(v)
+
+ case "UFix64":
+ v, err := cadence.NewUFix64(val)
+ if err != nil {
+ return nil, fmt.Errorf("could not parse unsigned fixed point integer: %w", err)
+ }
+ return v, nil
+
+ case "Fix64":
+ v, err := cadence.NewFix64(val)
+ if err != nil {
+ return nil, fmt.Errorf("could not parse fixed point integer: %w", err)
+ }
+ return v, nil
+
+ case "Address":
+ bytes, err := hex.DecodeString(val)
+ if err != nil {
+ return nil, fmt.Errorf("could not decode hex string: %w", err)
+ }
+ return cadence.BytesToAddress(bytes), nil
+
+ case "Bytes":
+ bytes, err := hex.DecodeString(val)
+ if err != nil {
+ return nil, fmt.Errorf("could not decode hex string: %w", err)
+ }
+ return cadence.NewBytes(bytes), nil
+
+ case "String":
+ return cadence.NewString(val)
+
+ default:
+ return nil, fmt.Errorf("unknown type for Cadence conversion (%s)", typ)
+ }
+}
diff --git a/models/convert/convert/cadence_test.go b/models/convert/convert/cadence_test.go
new file mode 100755
index 0000000..8faee04
--- /dev/null
+++ b/models/convert/convert/cadence_test.go
@@ -0,0 +1,275 @@
+// Copyright 2021 Optakt Labs OÜ
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+package convert_test
+
+import (
+ "math/big"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+
+ "github.com/onflow/cadence"
+
+ "github.com/optakt/flow-dps/models/convert"
+)
+
+func TestParseCadenceArgument(t *testing.T) {
+ tests := []struct {
+ name string
+ param string
+ wantArg cadence.Value
+ checkErr assert.ErrorAssertionFunc
+ }{
+ {
+ name: "handles invalid parameter format",
+ param: "test",
+ checkErr: assert.Error,
+ },
+ {
+ name: "parse valid boolean",
+ param: "Bool(true)",
+ wantArg: cadence.Bool(true),
+ checkErr: assert.NoError,
+ },
+ {
+ name: "parse invalid boolean",
+ param: "Bool(horse)",
+ checkErr: assert.Error,
+ },
+ {
+ name: "parse valid normal integer",
+ param: "Int(1337)",
+ wantArg: cadence.Int{Value: big.NewInt(1337)},
+ checkErr: assert.NoError,
+ },
+ {
+ name: "parse invalid normal integer",
+ param: "Int(a337)",
+ checkErr: assert.Error,
+ },
+ {
+ name: "parse valid short integer",
+ param: "Int8(127)",
+ wantArg: cadence.Int8(127),
+ checkErr: assert.NoError,
+ },
+ {
+ name: "parse invalid short integer",
+ param: "Int8(a27)",
+ checkErr: assert.Error,
+ },
+ {
+ name: "parse valid normal integer",
+ param: "Int16(1337)",
+ wantArg: cadence.Int16(1337),
+ checkErr: assert.NoError,
+ },
+ {
+ name: "parse invalid normal integer",
+ param: "Int16(a337)",
+ checkErr: assert.Error,
+ },
+ {
+ name: "parse valid 32-bit integer",
+ param: "Int32(1337)",
+ wantArg: cadence.Int32(1337),
+ checkErr: assert.NoError,
+ },
+ {
+ name: "parse invalid 32-bit integer",
+ param: "Int32(a337)",
+ checkErr: assert.Error,
+ },
+ {
+ name: "parse valid 64-bit integer",
+ param: "Int64(1337)",
+ wantArg: cadence.Int64(1337),
+ checkErr: assert.NoError,
+ },
+ {
+ name: "parse invalid 64-bit integer",
+ param: "Int64(a337)",
+ checkErr: assert.Error,
+ },
+ {
+ name: "parse valid 128-bit integer",
+ param: "Int128(1337)",
+ wantArg: cadence.Int128{Value: big.NewInt(1337)},
+ checkErr: assert.NoError,
+ },
+ {
+ name: "parse invalid 64-bit integer",
+ param: "Int128(a337)",
+ checkErr: assert.Error,
+ },
+ {
+ name: "parse valid 256-bit integer",
+ param: "Int256(1337)",
+ wantArg: cadence.Int256{Value: big.NewInt(1337)},
+ checkErr: assert.NoError,
+ },
+ {
+ name: "parse invalid 256-bit integer",
+ param: "Int256(a337)",
+ checkErr: assert.Error,
+ },
+ {
+ name: "parse valid unsigned integer",
+ param: "UInt(1337)",
+ wantArg: cadence.UInt{Value: big.NewInt(1337)},
+ checkErr: assert.NoError,
+ },
+ {
+ name: "parse invalid unsigned integer",
+ param: "UInt(-1337)",
+ checkErr: assert.Error,
+ },
+ {
+ name: "parse valid short unsigned integer",
+ param: "UInt8(127)",
+ wantArg: cadence.UInt8(127),
+ checkErr: assert.NoError,
+ },
+ {
+ name: "parse invalid short unsigned integer",
+ param: "UInt8(-127)",
+ checkErr: assert.Error,
+ },
+ {
+ name: "parse valid 16-bit unsigned integer",
+ param: "UInt16(1337)",
+ wantArg: cadence.UInt16(1337),
+ checkErr: assert.NoError,
+ },
+ {
+ name: "parse invalid 16-bit unsigned integer",
+ param: "UInt16(-1337)",
+ checkErr: assert.Error,
+ },
+ {
+ name: "parse valid 32-bit unsigned integer",
+ param: "UInt32(1337)",
+ wantArg: cadence.UInt32(1337),
+ checkErr: assert.NoError,
+ },
+ {
+ name: "parse invalid 32-bit unsigned integer",
+ param: "UInt32(-1337)",
+ checkErr: assert.Error,
+ },
+ {
+ name: "parse valid 64-bit unsigned integer",
+ param: "UInt64(1337)",
+ wantArg: cadence.UInt64(1337),
+ checkErr: assert.NoError,
+ },
+ {
+ name: "parse invalid 64-bit unsigned integer",
+ param: "UInt64(-1337)",
+ checkErr: assert.Error,
+ },
+ {
+ name: "parse valid 128-bit unsigned integer",
+ wantArg: cadence.UInt128{Value: big.NewInt(1337)},
+ param: "UInt128(1337)",
+ checkErr: assert.NoError,
+ },
+ {
+ name: "parse invalid 128-bit unsigned integer",
+ param: "UInt128(-1337)",
+ checkErr: assert.Error,
+ },
+ {
+ name: "parse valid big unsigned integer",
+ param: "UInt256(1337)",
+ wantArg: cadence.UInt256{Value: big.NewInt(1337)},
+ checkErr: assert.NoError,
+ },
+ {
+ name: "parse invalid big unsigned integer",
+ param: "UInt256(-1337)",
+ checkErr: assert.Error,
+ },
+ {
+ name: "parse valid unsigned fixed point",
+ param: "UFix64(13.37)",
+ wantArg: cadence.UFix64(1337000000),
+ checkErr: assert.NoError,
+ },
+ {
+ name: "parse invalid unsigned fixed point",
+ param: "UFix64(13,37)",
+ checkErr: assert.Error,
+ },
+ {
+ name: "parse valid fixed point",
+ param: "Fix64(13.37)",
+ wantArg: cadence.Fix64(1337000000),
+ checkErr: assert.NoError,
+ },
+ {
+ name: "parse invalid fixed point",
+ param: "Fix64(13,37)",
+ checkErr: assert.Error,
+ },
+ {
+ name: "parse valid address",
+ param: "Address(43AC64656E636521)",
+ wantArg: cadence.Address{0x43, 0xac, 0x64, 0x65, 0x6e, 0x63, 0x65, 0x21},
+ checkErr: assert.NoError,
+ },
+ {
+ name: "parse invalid address",
+ param: "Address(X3AC64656E636521)",
+ checkErr: assert.Error,
+ },
+ {
+ name: "parse valid bytes",
+ param: "Bytes(43AC64656E636521)",
+ wantArg: cadence.Bytes{0x43, 0xac, 0x64, 0x65, 0x6e, 0x63, 0x65, 0x21},
+ checkErr: assert.NoError,
+ },
+ {
+ name: "parse invalid bytes",
+ param: "Bytes(X3AC64656E636521)",
+ checkErr: assert.Error,
+ },
+ {
+ name: "parse valid string",
+ param: "String(MN7wrJh359Kx+J*#)",
+ wantArg: cadence.String("MN7wrJh359Kx+J*#"),
+ checkErr: assert.NoError,
+ },
+ {
+ name: "unsupported type",
+ param: "Doughnut(vanilla)",
+ checkErr: assert.Error,
+ },
+ }
+
+ for _, test := range tests {
+ test := test
+ t.Run(test.name, func(t *testing.T) {
+ t.Parallel()
+
+ gotArg, err := convert.ParseCadenceArgument(test.param)
+ test.checkErr(t, err)
+
+ if err == nil {
+ assert.Equal(t, test.wantArg, gotArg)
+ }
+ })
+ }
+}
diff --git a/models/convert/convert/hash.go b/models/convert/convert/hash.go
new file mode 100755
index 0000000..5a54057
--- /dev/null
+++ b/models/convert/convert/hash.go
@@ -0,0 +1,35 @@
+// Copyright 2021 Optakt Labs OÜ
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+package convert
+
+import (
+ "github.com/onflow/flow-go/model/flow"
+)
+
+// IDToHash converts a flow Identifier into a byte slice.
+func IDToHash(id flow.Identifier) []byte {
+ hash := make([]byte, 32)
+ copy(hash, id[:])
+
+ return hash
+}
+
+// CommitToHash converts a flow StateCommitment into a byte slice.
+func CommitToHash(commit flow.StateCommitment) []byte {
+ hash := make([]byte, 32)
+ copy(hash, commit[:])
+
+ return hash
+}
diff --git a/models/convert/convert/hash_test.go b/models/convert/convert/hash_test.go
new file mode 100755
index 0000000..aa7ad9c
--- /dev/null
+++ b/models/convert/convert/hash_test.go
@@ -0,0 +1,35 @@
+// Copyright 2021 Optakt Labs OÜ
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+package convert_test
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+
+ "github.com/optakt/flow-dps/models/convert"
+ "github.com/optakt/flow-rosetta/testing/mocks"
+)
+
+func TestIDToHash(t *testing.T) {
+ blockID := mocks.GenericHeader.ID()
+ got := convert.IDToHash(blockID)
+ assert.Equal(t, blockID[:], got)
+}
+
+func TestCommitToHash(t *testing.T) {
+ got := convert.CommitToHash(mocks.GenericCommit(0))
+ assert.Equal(t, mocks.ByteSlice(mocks.GenericCommit(0)), got)
+}
diff --git a/models/convert/convert/paths.go b/models/convert/convert/paths.go
new file mode 100755
index 0000000..3c90225
--- /dev/null
+++ b/models/convert/convert/paths.go
@@ -0,0 +1,45 @@
+// Copyright 2021 Optakt Labs OÜ
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+package convert
+
+import (
+ "fmt"
+
+ "github.com/onflow/flow-go/ledger"
+)
+
+// PathsToBytes converts a slice of ledger paths into a slice of byte slices.
+func PathsToBytes(paths []ledger.Path) [][]byte {
+ bb := make([][]byte, 0, len(paths))
+ for _, path := range paths {
+ b := make([]byte, len(path))
+ copy(b, path[:])
+ bb = append(bb, b)
+ }
+ return bb
+}
+
+// BytesToPaths converts a slice of byte slices into a slice of ledger paths.
+func BytesToPaths(bb [][]byte) ([]ledger.Path, error) {
+ paths := make([]ledger.Path, 0, len(bb))
+ for _, b := range bb {
+ path, err := ledger.ToPath(b)
+ if err != nil {
+ return nil, fmt.Errorf("could not convert path (%x): %w", b, err)
+ }
+ paths = append(paths, path)
+ }
+ return paths, nil
+}
diff --git a/models/convert/convert/paths_test.go b/models/convert/convert/paths_test.go
new file mode 100755
index 0000000..87b7ae4
--- /dev/null
+++ b/models/convert/convert/paths_test.go
@@ -0,0 +1,75 @@
+// Copyright 2021 Optakt Labs OÜ
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+package convert_test
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+
+ "github.com/optakt/flow-dps/models/convert"
+ "github.com/optakt/flow-rosetta/testing/mocks"
+)
+
+func TestPathsToBytes(t *testing.T) {
+ got := convert.PathsToBytes(mocks.GenericLedgerPaths(3))
+
+ assert.Equal(t, [][]byte{
+ mocks.ByteSlice(mocks.GenericLedgerPath(0)),
+ mocks.ByteSlice(mocks.GenericLedgerPath(1)),
+ mocks.ByteSlice(mocks.GenericLedgerPath(2)),
+ }, got)
+}
+
+func TestBytesToPaths(t *testing.T) {
+ t.Run("nominal case", func(t *testing.T) {
+ t.Parallel()
+
+ wantPaths := mocks.GenericLedgerPaths(3)
+
+ bb := [][]byte{
+ mocks.ByteSlice(mocks.GenericLedgerPath(0)),
+ mocks.ByteSlice(mocks.GenericLedgerPath(1)),
+ mocks.ByteSlice(mocks.GenericLedgerPath(2)),
+ }
+
+ got, err := convert.BytesToPaths(bb)
+
+ assert.NoError(t, err)
+ assert.Equal(t, wantPaths, got)
+ })
+
+ t.Run("incorrect-length paths should fail", func(t *testing.T) {
+ t.Parallel()
+
+ invalidPath := []byte{0x1a, 0x04, 0x57, 0x70, 0x00}
+
+ bb := [][]byte{invalidPath}
+ _, err := convert.BytesToPaths(bb)
+
+ assert.Error(t, err)
+ })
+
+ t.Run("empty paths should fail", func(t *testing.T) {
+ t.Parallel()
+
+ invalidPath := []byte("")
+
+ bb := [][]byte{invalidPath}
+ _, err := convert.BytesToPaths(bb)
+
+ assert.Error(t, err)
+ })
+}
diff --git a/models/convert/convert/types.go b/models/convert/convert/types.go
new file mode 100755
index 0000000..b9d3aae
--- /dev/null
+++ b/models/convert/convert/types.go
@@ -0,0 +1,44 @@
+// Copyright 2021 Optakt Labs OÜ
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+package convert
+
+import (
+ "time"
+
+ "github.com/onflow/flow-go/model/flow"
+)
+
+// TypesToStrings converts a slice of flow event types into a slice of strings.
+func TypesToStrings(types []flow.EventType) []string {
+ ss := make([]string, 0, len(types))
+ for _, typ := range types {
+ ss = append(ss, string(typ))
+ }
+ return ss
+}
+
+// StringsToTypes converts a slice of strings into a slice of flow event types.
+func StringsToTypes(ss []string) []flow.EventType {
+ types := make([]flow.EventType, 0, len(ss))
+ for _, s := range ss {
+ types = append(types, flow.EventType(s))
+ }
+ return types
+}
+
+// RosettaTime converts a time into a Rosetta-compatible timestamp.
+func RosettaTime(t time.Time) int64 {
+ return t.UnixNano() / 1_000_000
+}
diff --git a/models/convert/convert/types_test.go b/models/convert/convert/types_test.go
new file mode 100755
index 0000000..61f9e06
--- /dev/null
+++ b/models/convert/convert/types_test.go
@@ -0,0 +1,56 @@
+// Copyright 2021 Optakt Labs OÜ
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+package convert_test
+
+import (
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/assert"
+
+ "github.com/optakt/flow-dps/models/convert"
+ "github.com/optakt/flow-rosetta/testing/mocks"
+)
+
+func TestTypesToStrings(t *testing.T) {
+ types := mocks.GenericEventTypes(4)
+
+ got := convert.TypesToStrings(types)
+
+ for _, typ := range types {
+ assert.Contains(t, got, string(typ))
+ }
+}
+
+func TestStringsToTypes(t *testing.T) {
+ types := mocks.GenericEventTypes(4)
+
+ var ss []string
+ for _, typ := range types {
+ ss = append(ss, string(typ))
+ }
+
+ got := convert.StringsToTypes(ss)
+
+ assert.Equal(t, types, got)
+}
+
+func TestRosettaTime(t *testing.T) {
+ ti := time.Now()
+
+ got := convert.RosettaTime(ti)
+
+ assert.Equal(t, ti.UnixNano()/1_000_000, got)
+}
diff --git a/models/convert/convert/values.go b/models/convert/convert/values.go
new file mode 100755
index 0000000..0be088e
--- /dev/null
+++ b/models/convert/convert/values.go
@@ -0,0 +1,40 @@
+// Copyright 2021 Optakt Labs OÜ
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+package convert
+
+import (
+ "github.com/onflow/flow-go/ledger"
+)
+
+// ValuesToBytes converts a slice of ledger values into a slice of byte slices.
+func ValuesToBytes(values []ledger.Value) [][]byte {
+ bb := make([][]byte, 0, len(values))
+ for _, value := range values {
+ b := make([]byte, len(value))
+ copy(b, value[:])
+ bb = append(bb, b)
+ }
+ return bb
+}
+
+// BytesToValues converts a slice of byte slices into a slice of ledger values.
+func BytesToValues(bb [][]byte) []ledger.Value {
+ values := make([]ledger.Value, 0, len(bb))
+ for _, b := range bb {
+ value := ledger.Value(b)
+ values = append(values, value)
+ }
+ return values
+}
diff --git a/models/convert/convert/values_test.go b/models/convert/convert/values_test.go
new file mode 100755
index 0000000..47e2ce9
--- /dev/null
+++ b/models/convert/convert/values_test.go
@@ -0,0 +1,50 @@
+// Copyright 2021 Optakt Labs OÜ
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+package convert_test
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+
+ "github.com/optakt/flow-dps/models/convert"
+ "github.com/optakt/flow-rosetta/testing/mocks"
+)
+
+func TestValuesToBytes(t *testing.T) {
+ values := mocks.GenericLedgerValues(4)
+
+ var bb [][]byte
+ for _, val := range values {
+ bb = append(bb, val[:])
+ }
+
+ got := convert.ValuesToBytes(values)
+
+ assert.Equal(t, bb, got)
+}
+
+func TestBytesToValues(t *testing.T) {
+ values := mocks.GenericLedgerValues(4)
+
+ var bb [][]byte
+ for _, val := range values {
+ bb = append(bb, val[:])
+ }
+
+ got := convert.BytesToValues(bb)
+
+ assert.Equal(t, values, got)
+}
diff --git a/rosetta/configuration/configuration.go b/rosetta/configuration/configuration.go
new file mode 100755
index 0000000..c122675
--- /dev/null
+++ b/rosetta/configuration/configuration.go
@@ -0,0 +1,141 @@
+// Copyright 2021 Optakt Labs OÜ
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+package configuration
+
+import (
+ "github.com/onflow/flow-go/model/flow"
+
+ "github.com/optakt/flow-dps/models/dps"
+ "github.com/optakt/flow-rosetta/rosetta/failure"
+ "github.com/optakt/flow-rosetta/rosetta/identifier"
+ "github.com/optakt/flow-rosetta/rosetta/meta"
+)
+
+const (
+ blockchainUnknown = "network identifier has unknown blockchain field"
+ networkUnknown = "network identifier has unknown network field"
+)
+
+type Configuration struct {
+ network identifier.Network
+ version meta.Version
+ statuses []meta.StatusDefinition
+ operations []string
+ errors []meta.ErrorDefinition
+}
+
+// New returns the configuration for a given Flow chain.
+func New(chain flow.ChainID) *Configuration {
+
+ network := identifier.Network{
+ Blockchain: dps.FlowBlockchain,
+ Network: chain.String(),
+ }
+
+ version := meta.Version{
+ RosettaVersion: RosettaVersion,
+ NodeVersion: NodeVersion,
+ MiddlewareVersion: MiddlewareVersion,
+ }
+
+ statuses := []meta.StatusDefinition{
+ StatusCompleted,
+ }
+
+ operations := []string{
+ OperationTransfer,
+ }
+
+ errors := []meta.ErrorDefinition{
+ ErrorInternal,
+ ErrorInvalidEncoding,
+ ErrorInvalidFormat,
+ ErrorInvalidNetwork,
+ ErrorInvalidAccount,
+ ErrorInvalidCurrency,
+ ErrorInvalidBlock,
+ ErrorInvalidTransaction,
+ ErrorUnknownBlock,
+ ErrorUnknownCurrency,
+ ErrorUnknownTransaction,
+
+ ErrorInvalidIntent,
+ ErrorInvalidAuthorizers,
+ ErrorInvalidPayer,
+ ErrorInvalidProposer,
+ ErrorInvalidScript,
+ ErrorInvalidArguments,
+ ErrorInvalidAmount,
+ ErrorInvalidReceiver,
+ ErrorInvalidSignature,
+ ErrorInvalidKey,
+ ErrorInvalidPayload,
+ ErrorInvalidSignatures,
+ }
+
+ c := Configuration{
+ network: network,
+ version: version,
+ statuses: statuses,
+ operations: operations,
+ errors: errors,
+ }
+
+ return &c
+}
+
+// Network returns the configuration's network identifier.
+func (c *Configuration) Network() identifier.Network {
+ return c.network
+}
+
+// Version returns the configuration's version information.
+func (c *Configuration) Version() meta.Version {
+ return c.version
+}
+
+// Statuses returns the configuration's status definitions.
+func (c *Configuration) Statuses() []meta.StatusDefinition {
+ return c.statuses
+}
+
+// Operations returns the configuration's supported operations.
+func (c *Configuration) Operations() []string {
+ return c.operations
+}
+
+// Errors returns the configuration's error definitions.
+func (c *Configuration) Errors() []meta.ErrorDefinition {
+ return c.errors
+}
+
+// Check verifies whether a network identifier matches with the configured one.
+func (c *Configuration) Check(network identifier.Network) error {
+ if network.Blockchain != c.network.Blockchain {
+ return failure.InvalidBlockchain{
+ HaveBlockchain: network.Blockchain,
+ WantBlockchain: c.network.Blockchain,
+ Description: failure.NewDescription(blockchainUnknown),
+ }
+ }
+ if network.Network != c.network.Network {
+ return failure.InvalidNetwork{
+ HaveNetwork: network.Network,
+ WantNetwork: c.network.Network,
+ Description: failure.NewDescription(networkUnknown),
+ }
+ }
+ return nil
+}
diff --git a/rosetta/configuration/errors.go b/rosetta/configuration/errors.go
new file mode 100755
index 0000000..a1f15ea
--- /dev/null
+++ b/rosetta/configuration/errors.go
@@ -0,0 +1,48 @@
+// Copyright 2021 Optakt Labs OÜ
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+package configuration
+
+import (
+ "github.com/optakt/flow-rosetta/rosetta/meta"
+)
+
+var (
+ // Data API specific errors.
+ ErrorInternal = meta.ErrorDefinition{Code: 1, Message: "internal error", Retriable: false}
+ ErrorInvalidEncoding = meta.ErrorDefinition{Code: 2, Message: "invalid request encoding", Retriable: false}
+ ErrorInvalidFormat = meta.ErrorDefinition{Code: 3, Message: "invalid request format", Retriable: false}
+ ErrorInvalidNetwork = meta.ErrorDefinition{Code: 4, Message: "invalid network identifier", Retriable: false}
+ ErrorInvalidAccount = meta.ErrorDefinition{Code: 5, Message: "invalid account identifier", Retriable: false}
+ ErrorInvalidCurrency = meta.ErrorDefinition{Code: 6, Message: "invalid currency identifier", Retriable: false}
+ ErrorInvalidBlock = meta.ErrorDefinition{Code: 7, Message: "invalid block identifier", Retriable: false}
+ ErrorInvalidTransaction = meta.ErrorDefinition{Code: 8, Message: "invalid transaction identifier", Retriable: false}
+ ErrorUnknownBlock = meta.ErrorDefinition{Code: 9, Message: "unknown block identifier", Retriable: true}
+ ErrorUnknownCurrency = meta.ErrorDefinition{Code: 10, Message: "unknown currency identifier", Retriable: false}
+ ErrorUnknownTransaction = meta.ErrorDefinition{Code: 11, Message: "unknown block transaction", Retriable: false}
+
+ // Construction API specific errors.
+ ErrorInvalidIntent = meta.ErrorDefinition{Code: 12, Message: "invalid transaction intent", Retriable: false}
+ ErrorInvalidAuthorizers = meta.ErrorDefinition{Code: 13, Message: "invalid transaction authorizers", Retriable: false}
+ ErrorInvalidPayer = meta.ErrorDefinition{Code: 14, Message: "invalid transaction payer", Retriable: false}
+ ErrorInvalidProposer = meta.ErrorDefinition{Code: 15, Message: "invalid transaction proposer", Retriable: false}
+ ErrorInvalidScript = meta.ErrorDefinition{Code: 16, Message: "invalid transaction script", Retriable: false}
+ ErrorInvalidArguments = meta.ErrorDefinition{Code: 17, Message: "invalid transaction arguments", Retriable: false}
+ ErrorInvalidAmount = meta.ErrorDefinition{Code: 18, Message: "invalid transaction amount", Retriable: false}
+ ErrorInvalidReceiver = meta.ErrorDefinition{Code: 19, Message: "invalid transaction recipient", Retriable: false}
+ ErrorInvalidSignature = meta.ErrorDefinition{Code: 20, Message: "invalid transaction signature", Retriable: false}
+ ErrorInvalidKey = meta.ErrorDefinition{Code: 21, Message: "invalid transaction signer key", Retriable: false}
+ ErrorInvalidPayload = meta.ErrorDefinition{Code: 22, Message: "invalid transaction payload", Retriable: false}
+ ErrorInvalidSignatures = meta.ErrorDefinition{Code: 23, Message: "invalid transaction signatures", Retriable: false}
+)
diff --git a/rosetta/configuration/operations.go b/rosetta/configuration/operations.go
new file mode 100755
index 0000000..0985a0d
--- /dev/null
+++ b/rosetta/configuration/operations.go
@@ -0,0 +1,20 @@
+// Copyright 2021 Optakt Labs OÜ
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+package configuration
+
+// Supported operations.
+const (
+ OperationTransfer = "TRANSFER"
+)
diff --git a/rosetta/configuration/statuses.go b/rosetta/configuration/statuses.go
new file mode 100755
index 0000000..aa17bda
--- /dev/null
+++ b/rosetta/configuration/statuses.go
@@ -0,0 +1,24 @@
+// Copyright 2021 Optakt Labs OÜ
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+package configuration
+
+import (
+ "github.com/optakt/flow-rosetta/rosetta/meta"
+)
+
+// Status definitions.
+var (
+ StatusCompleted = meta.StatusDefinition{Status: "COMPLETED", Successful: true}
+)
diff --git a/rosetta/configuration/version.go b/rosetta/configuration/version.go
new file mode 100755
index 0000000..2ffd626
--- /dev/null
+++ b/rosetta/configuration/version.go
@@ -0,0 +1,21 @@
+// Copyright 2021 Optakt Labs OÜ
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+package configuration
+
+const (
+ RosettaVersion = "1.4.10"
+ NodeVersion = "0.21.2"
+ MiddlewareVersion = "1.3.3"
+)
diff --git a/rosetta/converter/converter.go b/rosetta/converter/converter.go
new file mode 100755
index 0000000..f99f31d
--- /dev/null
+++ b/rosetta/converter/converter.go
@@ -0,0 +1,133 @@
+// Copyright 2021 Optakt Labs OÜ
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+package converter
+
+import (
+ "fmt"
+ "strconv"
+
+ "github.com/onflow/cadence"
+ "github.com/onflow/cadence/encoding/json"
+ "github.com/onflow/flow-go/model/flow"
+
+ "github.com/optakt/flow-dps/models/dps"
+ "github.com/optakt/flow-rosetta/rosetta/identifier"
+ "github.com/optakt/flow-rosetta/rosetta/object"
+ "github.com/optakt/flow-rosetta/rosetta/retriever"
+)
+
+// Converter converts Flow Events into Rosetta Operations.
+type Converter struct {
+ deposit flow.EventType
+ withdrawal flow.EventType
+}
+
+// New instantiates and returns a new converter using the given Generator.
+func New(gen Generator) (*Converter, error) {
+ deposit, err := gen.TokensDeposited(dps.FlowSymbol)
+ if err != nil {
+ return nil, fmt.Errorf("could not generate deposit event type: %w", err)
+ }
+ withdrawal, err := gen.TokensWithdrawn(dps.FlowSymbol)
+ if err != nil {
+ return nil, fmt.Errorf("could not generate withdrawal event type: %w", err)
+ }
+
+ c := Converter{
+ deposit: flow.EventType(deposit),
+ withdrawal: flow.EventType(withdrawal),
+ }
+
+ return &c, nil
+}
+
+// EventToOperation converts a flow.Event into a Rosetta Operation.
+func (c *Converter) EventToOperation(event flow.Event) (operation *object.Operation, err error) {
+
+ // Decode the event payload into a Cadence value and cast it to a Cadence event.
+ value, err := json.Decode(event.Payload)
+ if err != nil {
+ return nil, fmt.Errorf("could not decode event: %w", err)
+ }
+ e, ok := value.(cadence.Event)
+ if !ok {
+ return nil, fmt.Errorf("could not cast event: %w", err)
+ }
+
+ // Ensure that there are the correct amount of fields.
+ if len(e.Fields) != 2 {
+ return nil, fmt.Errorf("invalid number of fields (want: %d, have: %d)", 2, len(e.Fields))
+ }
+
+ // The first field is always the amount and the second one the address.
+ // The types coming from Cadence are not native Flow types, so primitive types
+ // are needed before they can be converted into proper Flow types.
+ vAmount := e.Fields[0].ToGoValue()
+ uAmount, ok := vAmount.(uint64)
+ if !ok {
+ return nil, fmt.Errorf("could not cast amount (%T)", vAmount)
+ }
+
+ vAddress := e.Fields[1].ToGoValue()
+
+ // Sometimes an event is not associated with an account. Ignore these events
+ // as they refer to intermediary vaults.
+ if vAddress == nil {
+ return nil, retriever.ErrNoAddress
+ }
+
+ bAddress, ok := vAddress.([flow.AddressLength]byte)
+ if !ok {
+ return nil, fmt.Errorf("could not cast address (%T)", vAddress)
+ }
+
+ // Convert the amount to a signed integer that it can be inverted.
+ amount := int64(uAmount)
+ // Convert the address bytes into a native Flow address.
+ address := flow.Address(bAddress)
+
+ netIndex := uint(event.EventIndex)
+ op := object.Operation{
+ ID: identifier.Operation{
+ NetworkIndex: &netIndex,
+ },
+ Status: dps.StatusCompleted,
+ AccountID: identifier.Account{
+ Address: address.String(),
+ },
+ }
+
+ switch event.Type {
+ case c.deposit:
+ op.Type = dps.OperationTransfer
+
+ // In the case of a withdrawal, invert the amount value.
+ case c.withdrawal:
+ op.Type = dps.OperationTransfer
+ amount = -amount
+ default:
+ return nil, retriever.ErrNotSupported
+ }
+
+ op.Amount = object.Amount{
+ Value: strconv.FormatInt(amount, 10),
+ Currency: identifier.Currency{
+ Symbol: dps.FlowSymbol,
+ Decimals: dps.FlowDecimals,
+ },
+ }
+
+ return &op, nil
+}
diff --git a/rosetta/converter/converter_internal_test.go b/rosetta/converter/converter_internal_test.go
new file mode 100755
index 0000000..1fd1c05
--- /dev/null
+++ b/rosetta/converter/converter_internal_test.go
@@ -0,0 +1,354 @@
+// Copyright 2021 Optakt Labs OÜ
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+package converter
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+
+ "github.com/onflow/cadence"
+ "github.com/onflow/cadence/encoding/json"
+ "github.com/onflow/cadence/runtime/tests/utils"
+ "github.com/onflow/flow-go/model/flow"
+
+ "github.com/optakt/flow-dps/models/dps"
+ "github.com/optakt/flow-rosetta/rosetta/identifier"
+ "github.com/optakt/flow-rosetta/rosetta/object"
+ "github.com/optakt/flow-rosetta/rosetta/retriever"
+ "github.com/optakt/flow-rosetta/testing/mocks"
+)
+
+func TestNew(t *testing.T) {
+ t.Run("nominal case", func(t *testing.T) {
+ generator := mocks.BaselineGenerator(t)
+ generator.TokensDepositedFunc = func(symbol string) (string, error) {
+ assert.Equal(t, dps.FlowSymbol, symbol)
+ return string(mocks.GenericEventType(0)), nil
+ }
+ generator.TokensWithdrawnFunc = func(symbol string) (string, error) {
+ assert.Equal(t, dps.FlowSymbol, symbol)
+ return string(mocks.GenericEventType(1)), nil
+ }
+
+ cvt, err := New(generator)
+
+ require.NoError(t, err)
+ assert.Equal(t, cvt.deposit, mocks.GenericEventType(0))
+ assert.Equal(t, cvt.withdrawal, mocks.GenericEventType(1))
+ })
+
+ t.Run("handles generator failure for deposit event type", func(t *testing.T) {
+ generator := mocks.BaselineGenerator(t)
+ generator.TokensDepositedFunc = func(symbol string) (string, error) {
+ return "", mocks.GenericError
+ }
+
+ cvt, err := New(generator)
+
+ assert.Error(t, err)
+ assert.Nil(t, cvt)
+ })
+
+ t.Run("handles generator failure for withdrawal event type", func(t *testing.T) {
+ generator := mocks.BaselineGenerator(t)
+ generator.TokensWithdrawnFunc = func(symbol string) (string, error) {
+ return "", mocks.GenericError
+ }
+
+ cvt, err := New(generator)
+
+ assert.Error(t, err)
+ assert.Nil(t, cvt)
+ })
+}
+
+func TestConverter_EventToOperation(t *testing.T) {
+ depositType := &cadence.EventType{
+ Location: utils.TestLocation,
+ QualifiedIdentifier: string(mocks.GenericEventType(0)),
+ Fields: []cadence.Field{
+ {
+ Identifier: "amount",
+ Type: cadence.UInt64Type{},
+ },
+ {
+ Identifier: "address",
+ Type: cadence.AddressType{},
+ },
+ },
+ }
+ depositEvent := cadence.NewEvent(
+ []cadence.Value{
+ cadence.NewUInt64(42),
+ cadence.NewAddress([8]byte{1, 2, 3, 4, 5, 6, 7, 8}),
+ },
+ ).WithType(depositType)
+ depositEventPayload := json.MustEncode(depositEvent)
+
+ withdrawalType := &cadence.EventType{
+ Location: utils.TestLocation,
+ QualifiedIdentifier: string(mocks.GenericEventType(1)),
+ Fields: []cadence.Field{
+ {
+ Identifier: "amount",
+ Type: cadence.UInt64Type{},
+ },
+ {
+ Identifier: "address",
+ Type: cadence.AddressType{},
+ },
+ },
+ }
+ withdrawalEvent := cadence.NewEvent(
+ []cadence.Value{
+ cadence.NewUInt64(42),
+ cadence.NewAddress([8]byte{2, 3, 4, 5, 6, 7, 8, 9}),
+ },
+ ).WithType(withdrawalType)
+ withdrawalEventPayload := json.MustEncode(withdrawalEvent)
+
+ depositNetIndex := uint(1)
+ testDepositOp := object.Operation{
+ ID: identifier.Operation{
+ NetworkIndex: &depositNetIndex,
+ },
+ Type: dps.OperationTransfer,
+ Status: dps.StatusCompleted,
+ AccountID: identifier.Account{
+ Address: "0102030405060708",
+ },
+ Amount: object.Amount{
+ Value: "42",
+ Currency: identifier.Currency{
+ Symbol: dps.FlowSymbol,
+ Decimals: dps.FlowDecimals,
+ },
+ },
+ }
+ withdrawalNetIndex := uint(2)
+ testWithdrawalOp := object.Operation{
+ ID: identifier.Operation{
+ NetworkIndex: &withdrawalNetIndex,
+ },
+ Type: dps.OperationTransfer,
+ Status: dps.StatusCompleted,
+ AccountID: identifier.Account{
+ Address: "0203040506070809",
+ },
+ Amount: object.Amount{
+ Value: "-42",
+ Currency: identifier.Currency{
+ Symbol: dps.FlowSymbol,
+ Decimals: dps.FlowDecimals,
+ },
+ },
+ }
+
+ id, err := flow.HexStringToIdentifier("a4c4194eae1a2dd0de4f4d51a884db4255bf265a40ddd98477a1d60ef45909ec")
+ require.NoError(t, err)
+
+ threeFieldsType := &cadence.EventType{
+ Location: utils.TestLocation,
+ QualifiedIdentifier: "test",
+ Fields: []cadence.Field{
+ {
+ Identifier: "testField1",
+ Type: cadence.UInt64Type{},
+ },
+ {
+ Identifier: "testField2",
+ Type: cadence.UInt64Type{},
+ },
+ {
+ Identifier: "testField3",
+ Type: cadence.UInt64Type{},
+ },
+ },
+ }
+ threeFieldsEvent := cadence.NewEvent(
+ []cadence.Value{
+ cadence.NewUInt64(42),
+ cadence.NewUInt64(42),
+ cadence.NewUInt64(42),
+ },
+ ).WithType(threeFieldsType)
+ threeFieldsEventPayload := json.MustEncode(threeFieldsEvent)
+
+ missingAmountEventType := &cadence.EventType{
+ Location: utils.TestLocation,
+ QualifiedIdentifier: "test",
+ Fields: []cadence.Field{
+ {
+ Identifier: "address",
+ Type: cadence.AddressType{},
+ },
+ {
+ Identifier: "testField",
+ Type: cadence.AddressType{},
+ },
+ },
+ }
+ missingAmountEvent := cadence.NewEvent(
+ []cadence.Value{
+ cadence.NewAddress([8]byte{1, 2, 3, 4, 5, 6, 7, 8}),
+ cadence.NewAddress([8]byte{1, 2, 3, 4, 5, 6, 7, 8}),
+ },
+ ).WithType(missingAmountEventType)
+ missingAmountEventPayload := json.MustEncode(missingAmountEvent)
+
+ missingAddressEventType := &cadence.EventType{
+ Location: utils.TestLocation,
+ QualifiedIdentifier: "test",
+ Fields: []cadence.Field{
+ {
+ Identifier: "amount",
+ Type: cadence.UInt64Type{},
+ },
+ {
+ Identifier: "amount",
+ Type: cadence.UInt64Type{},
+ },
+ },
+ }
+ missingAddressEvent := cadence.NewEvent(
+ []cadence.Value{
+ cadence.NewUInt64(42),
+ cadence.NewUInt64(42),
+ },
+ ).WithType(missingAddressEventType)
+ missingAddressEventPayload := json.MustEncode(missingAddressEvent)
+
+ nilAddressEvent := cadence.NewEvent(
+ []cadence.Value{
+ cadence.NewUInt64(42),
+ cadence.NewOptional(nil),
+ },
+ ).WithType(withdrawalType)
+ nilAddressPayload := json.MustEncode(nilAddressEvent)
+
+ tests := []struct {
+ name string
+
+ event flow.Event
+
+ wantErr assert.ErrorAssertionFunc
+ wantSentinel error
+ wantOperation *object.Operation
+ }{
+ {
+ name: "nominal case with deposit event",
+
+ event: flow.Event{
+ TransactionID: id,
+ Type: mocks.GenericEventType(0),
+ Payload: depositEventPayload,
+ EventIndex: 1,
+ },
+
+ wantErr: assert.NoError,
+ wantOperation: &testDepositOp,
+ },
+ {
+ name: "nominal case with withdrawal event",
+
+ event: flow.Event{
+ TransactionID: id,
+ Type: mocks.GenericEventType(1),
+ Payload: withdrawalEventPayload,
+ EventIndex: 2,
+ },
+
+ wantErr: assert.NoError,
+ wantOperation: &testWithdrawalOp,
+ },
+ {
+ name: "unsupported event type",
+
+ event: flow.Event{
+ TransactionID: id,
+ Type: flow.EventType("irrelevant"),
+ Payload: withdrawalEventPayload,
+ },
+
+ wantErr: assert.Error,
+ wantSentinel: retriever.ErrNotSupported,
+ },
+ {
+ name: "wrong amount of fields",
+
+ event: flow.Event{
+ Type: mocks.GenericEventType(0),
+ Payload: threeFieldsEventPayload,
+ },
+
+ wantErr: assert.Error,
+ },
+ {
+ name: "missing amount field",
+
+ event: flow.Event{
+ Type: mocks.GenericEventType(0),
+ Payload: missingAmountEventPayload,
+ },
+
+ wantErr: assert.Error,
+ },
+ {
+ name: "missing address field",
+
+ event: flow.Event{
+ Type: mocks.GenericEventType(0),
+ Payload: missingAddressEventPayload,
+ },
+
+ wantErr: assert.Error,
+ },
+ {
+ name: "nil address field",
+
+ event: flow.Event{
+ TransactionID: id,
+ Type: mocks.GenericEventType(0),
+ Payload: nilAddressPayload,
+ },
+
+ wantErr: assert.Error,
+ wantSentinel: retriever.ErrNoAddress,
+ },
+ }
+
+ for _, test := range tests {
+ test := test
+ t.Run(test.name, func(t *testing.T) {
+ t.Parallel()
+
+ cvt := &Converter{
+ deposit: mocks.GenericEventType(0),
+ withdrawal: mocks.GenericEventType(1),
+ }
+
+ got, err := cvt.EventToOperation(test.event)
+
+ test.wantErr(t, err)
+ if test.wantSentinel != nil {
+ assert.ErrorIs(t, err, test.wantSentinel)
+ }
+
+ assert.Equal(t, test.wantOperation, got)
+ })
+ }
+}
diff --git a/rosetta/converter/generator.go b/rosetta/converter/generator.go
new file mode 100755
index 0000000..e951998
--- /dev/null
+++ b/rosetta/converter/generator.go
@@ -0,0 +1,22 @@
+// Copyright 2021 Optakt Labs OÜ
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+package converter
+
+// Generator represents something that can generate scripts for retrieving the amounts
+// deposited and withdrawn for a given token.
+type Generator interface {
+ TokensDeposited(symbol string) (string, error)
+ TokensWithdrawn(symbol string) (string, error)
+}
diff --git a/rosetta/failure/description.go b/rosetta/failure/description.go
new file mode 100755
index 0000000..68ac13d
--- /dev/null
+++ b/rosetta/failure/description.go
@@ -0,0 +1,126 @@
+// Copyright 2021 Optakt Labs OÜ
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+package failure
+
+import (
+ "fmt"
+ "strconv"
+ "strings"
+
+ "github.com/onflow/flow-go/model/flow"
+)
+
+// Description is the description of a Rosetta API failure.
+type Description struct {
+ Text string
+ Fields Fields
+}
+
+// NewDescription returns a new description from a given text and fields.
+func NewDescription(text string, fields ...FieldFunc) Description {
+ d := Description{
+ Text: text,
+ Fields: []Field{},
+ }
+ for _, field := range fields {
+ field(&d.Fields)
+ }
+ return d
+}
+
+// String implements the Stringer interface.
+func (d Description) String() string {
+ if len(d.Fields) == 0 {
+ return d.Text
+ }
+ return fmt.Sprintf("%s (%s)", d.Text, d.Fields)
+}
+
+// Field is a key/value pair used to add context to a Description.
+type Field struct {
+ Key string
+ Val interface{}
+}
+
+// Fields is a slice of Field.
+type Fields []Field
+
+// Iterate is used to give external access to the fields to consumers of this package.
+func (f Fields) Iterate(handle func(key string, val interface{})) {
+ for _, field := range f {
+ handle(field.Key, field.Val)
+ }
+}
+
+// String implements the Stringer interface.
+func (f Fields) String() string {
+ parts := make([]string, 0, len(f))
+ for _, field := range f {
+ part := fmt.Sprintf("%s: %s", field.Key, field.Val)
+ parts = append(parts, part)
+ }
+ return strings.Join(parts, ", ")
+}
+
+// FieldFunc is a function that is applied to a Fields pointer.
+type FieldFunc func(*Fields)
+
+// WithErr returns a function that adds an error value to a slice of Fields.
+func WithErr(err error) FieldFunc {
+ return func(f *Fields) {
+ field := Field{Key: "error", Val: err.Error()}
+ *f = append(*f, field)
+ }
+}
+
+// WithInt returns a function that adds an integer value to a slice of Fields.
+func WithInt(key string, val int) FieldFunc {
+ return func(f *Fields) {
+ field := Field{Key: key, Val: strconv.FormatInt(int64(val), 10)}
+ *f = append(*f, field)
+ }
+}
+
+// WithUint64 returns a function that adds an unsigned integer value to a slice of Fields.
+func WithUint64(key string, val uint64) FieldFunc {
+ return func(f *Fields) {
+ field := Field{Key: key, Val: strconv.FormatUint(val, 10)}
+ *f = append(*f, field)
+ }
+}
+
+// WithID returns a function that adds a flow Identifier value to a slice of Fields.
+func WithID(key string, val flow.Identifier) FieldFunc {
+ return func(f *Fields) {
+ field := Field{Key: key, Val: val}
+ *f = append(*f, field)
+ }
+}
+
+// WithString returns a function that adds a string value to a slice of Fields.
+func WithString(key string, val string) FieldFunc {
+ return func(f *Fields) {
+ field := Field{Key: key, Val: val}
+ *f = append(*f, field)
+ }
+}
+
+// WithStrings returns a function that adds a slice of strings to a slice of Fields.
+func WithStrings(key string, vals ...string) FieldFunc {
+ return func(f *Fields) {
+ field := Field{Key: key, Val: vals}
+ *f = append(*f, field)
+ }
+}
diff --git a/rosetta/failure/description_test.go b/rosetta/failure/description_test.go
new file mode 100755
index 0000000..b2c0301
--- /dev/null
+++ b/rosetta/failure/description_test.go
@@ -0,0 +1,67 @@
+// Copyright 2021 Optakt Labs OÜ
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+package failure_test
+
+import (
+ "fmt"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+
+ "github.com/optakt/flow-dps/models/convert"
+ "github.com/optakt/flow-dps/models/dps"
+ "github.com/optakt/flow-rosetta/rosetta/failure"
+ "github.com/optakt/flow-rosetta/testing/mocks"
+)
+
+func TestDescription(t *testing.T) {
+ descBody := "test"
+ header := mocks.GenericHeader
+ index := 84
+ network := dps.FlowTestnet.String()
+ eventTypes := convert.TypesToStrings(mocks.GenericEventTypes(4))
+
+ t.Run("full description with fields", func(t *testing.T) {
+ t.Parallel()
+
+ desc := failure.NewDescription(
+ descBody,
+ failure.WithErr(mocks.GenericError),
+ failure.WithUint64("height", header.Height),
+ failure.WithID("blockID", header.ID()),
+ failure.WithInt("index", index),
+ failure.WithString("network", network),
+ failure.WithStrings("types", eventTypes...),
+ )
+
+ assert.Equal(t, desc.Text, descBody)
+ assert.NotEqual(t, desc.String(), descBody)
+ assert.Contains(t, desc.Fields.String(), mocks.GenericError.Error())
+ assert.Contains(t, desc.Fields.String(), fmt.Sprintf("height: %v", mocks.GenericHeight))
+ assert.Contains(t, desc.Fields.String(), fmt.Sprintf("blockID: %v", header.ID()))
+ assert.Contains(t, desc.Fields.String(), fmt.Sprintf("index: %v", index))
+ assert.Contains(t, desc.Fields.String(), fmt.Sprintf("network: %v", network))
+ assert.Contains(t, desc.Fields.String(), fmt.Sprintf("types: %v", eventTypes))
+ })
+
+ t.Run("no fields", func(t *testing.T) {
+ t.Parallel()
+
+ desc := failure.NewDescription(descBody)
+
+ assert.Equal(t, desc.Text, descBody)
+ assert.Equal(t, desc.String(), descBody)
+ })
+}
diff --git a/rosetta/failure/incomplete_block.go b/rosetta/failure/incomplete_block.go
new file mode 100755
index 0000000..e3b0e16
--- /dev/null
+++ b/rosetta/failure/incomplete_block.go
@@ -0,0 +1,30 @@
+// Copyright 2021 Optakt Labs OÜ
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+package failure
+
+import (
+ "fmt"
+)
+
+// IncompleteBlock is the error for an incomplete block identifier, missing
+// either the hash or the index.
+type IncompleteBlock struct {
+ Description Description
+}
+
+// Error implements the error interface.
+func (i IncompleteBlock) Error() string {
+ return fmt.Sprintf("incomplete block: %s", i.Description)
+}
diff --git a/rosetta/failure/invalid_account.go b/rosetta/failure/invalid_account.go
new file mode 100755
index 0000000..bc5522d
--- /dev/null
+++ b/rosetta/failure/invalid_account.go
@@ -0,0 +1,30 @@
+// Copyright 2021 Optakt Labs OÜ
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+package failure
+
+import (
+ "fmt"
+)
+
+// InvalidAccount is the error for an account with an invalid identifier.
+type InvalidAccount struct {
+ Description Description
+ Address string
+}
+
+// Error implements the error interface.
+func (i InvalidAccount) Error() string {
+ return fmt.Sprintf("invalid account (address: %s): %s", i.Address, i.Description)
+}
diff --git a/rosetta/failure/invalid_account_address.go b/rosetta/failure/invalid_account_address.go
new file mode 100755
index 0000000..8bfd92c
--- /dev/null
+++ b/rosetta/failure/invalid_account_address.go
@@ -0,0 +1,31 @@
+// Copyright 2021 Optakt Labs OÜ
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+package failure
+
+import (
+ "fmt"
+)
+
+// InvalidAccountAddress is the error for an account address of invalid length.
+type InvalidAccountAddress struct {
+ Description Description
+ WantLength int
+ HaveLength int
+}
+
+// Error implements the error interface.
+func (i InvalidAccountAddress) Error() string {
+ return fmt.Sprintf("invalid account address length (want: %d, have: %d): %s", i.WantLength, i.HaveLength, i.Description)
+}
diff --git a/rosetta/failure/invalid_amount.go b/rosetta/failure/invalid_amount.go
new file mode 100755
index 0000000..9f461ea
--- /dev/null
+++ b/rosetta/failure/invalid_amount.go
@@ -0,0 +1,30 @@
+// Copyright 2021 Optakt Labs OÜ
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+package failure
+
+import (
+ "fmt"
+)
+
+// InvalidAmount is the error for an invalid transaction amount.
+type InvalidAmount struct {
+ Description Description
+ Amount string
+}
+
+// Error implements the error interface.
+func (i InvalidAmount) Error() string {
+ return fmt.Sprintf("invalid transaction amount (amount: %s): %s", i.Amount, i.Description)
+}
diff --git a/rosetta/failure/invalid_arguments.go b/rosetta/failure/invalid_arguments.go
new file mode 100755
index 0000000..73c8182
--- /dev/null
+++ b/rosetta/failure/invalid_arguments.go
@@ -0,0 +1,31 @@
+// Copyright 2021 Optakt Labs OÜ
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+package failure
+
+import (
+ "fmt"
+)
+
+// InvalidArguments is the error for an invalid number of arguments.
+type InvalidArguments struct {
+ Description Description
+ Have uint
+ Want uint
+}
+
+// Error implements the error interface.
+func (i InvalidArguments) Error() string {
+ return fmt.Sprintf("invalid transaction arguments (have: %d, want: %d): %s", i.Have, i.Want, i.Description)
+}
diff --git a/rosetta/failure/invalid_authorizers.go b/rosetta/failure/invalid_authorizers.go
new file mode 100755
index 0000000..f93ddc9
--- /dev/null
+++ b/rosetta/failure/invalid_authorizers.go
@@ -0,0 +1,31 @@
+// Copyright 2021 Optakt Labs OÜ
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+package failure
+
+import (
+ "fmt"
+)
+
+// InvalidAuthorizers is the error for an invalid number of authorizers.
+type InvalidAuthorizers struct {
+ Description Description
+ Have uint
+ Want uint
+}
+
+// Error implements the error interface.
+func (i InvalidAuthorizers) Error() string {
+ return fmt.Sprintf("invalid number of authorizers (have: %d, want: %d): %s", i.Have, i.Want, i.Description)
+}
diff --git a/rosetta/failure/invalid_block.go b/rosetta/failure/invalid_block.go
new file mode 100755
index 0000000..978a7c6
--- /dev/null
+++ b/rosetta/failure/invalid_block.go
@@ -0,0 +1,29 @@
+// Copyright 2021 Optakt Labs OÜ
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+package failure
+
+import (
+ "fmt"
+)
+
+// InvalidBlock is the error for block with an invalid identifier.
+type InvalidBlock struct {
+ Description Description
+}
+
+// Error implements the error interface.
+func (i InvalidBlock) Error() string {
+ return fmt.Sprintf("invalid block: %s", i.Description)
+}
diff --git a/rosetta/failure/invalid_block_hash.go b/rosetta/failure/invalid_block_hash.go
new file mode 100755
index 0000000..aa8262d
--- /dev/null
+++ b/rosetta/failure/invalid_block_hash.go
@@ -0,0 +1,31 @@
+// Copyright 2021 Optakt Labs OÜ
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+package failure
+
+import (
+ "fmt"
+)
+
+// InvalidBlockHash is the error for a block hash of invalid length.
+type InvalidBlockHash struct {
+ Description Description
+ WantLength int
+ HaveLength int
+}
+
+// Error implements the error interface.
+func (i InvalidBlockHash) Error() string {
+ return fmt.Sprintf("invalid block hash length (want: %d, have: %d): %s", i.WantLength, i.HaveLength, i.Description)
+}
diff --git a/rosetta/failure/invalid_blockchain.go b/rosetta/failure/invalid_blockchain.go
new file mode 100755
index 0000000..c5c0380
--- /dev/null
+++ b/rosetta/failure/invalid_blockchain.go
@@ -0,0 +1,29 @@
+// Copyright 2021 Optakt Labs OÜ
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+package failure
+
+import (
+ "fmt"
+)
+
+type InvalidBlockchain struct {
+ Description Description
+ HaveBlockchain string
+ WantBlockchain string
+}
+
+func (i InvalidBlockchain) Error() string {
+ return fmt.Sprintf("invalid blockchain (have: %s, want: %s): %s", i.HaveBlockchain, i.WantBlockchain, i.Description)
+}
diff --git a/rosetta/failure/invalid_currency.go b/rosetta/failure/invalid_currency.go
new file mode 100755
index 0000000..556eac7
--- /dev/null
+++ b/rosetta/failure/invalid_currency.go
@@ -0,0 +1,31 @@
+// Copyright 2021 Optakt Labs OÜ
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+package failure
+
+import (
+ "fmt"
+)
+
+// InvalidCurrency is the error for a currency with missing or unexpected decimals.
+type InvalidCurrency struct {
+ Description Description
+ Symbol string
+ Decimals uint
+}
+
+// Error implements the error interface.
+func (i InvalidCurrency) Error() string {
+ return fmt.Sprintf("invalid currency (symbol: %s, decimals: %d): %s", i.Symbol, i.Decimals, i.Description)
+}
diff --git a/rosetta/failure/invalid_intent.go b/rosetta/failure/invalid_intent.go
new file mode 100755
index 0000000..d0ad914
--- /dev/null
+++ b/rosetta/failure/invalid_intent.go
@@ -0,0 +1,29 @@
+// Copyright 2021 Optakt Labs OÜ
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+package failure
+
+import (
+ "fmt"
+)
+
+// InvalidIntent is the error for an invalid transaction intent.
+type InvalidIntent struct {
+ Description Description
+}
+
+// Error implements the error interface.
+func (i InvalidIntent) Error() string {
+ return fmt.Sprintf("invalid transaction intent: %s", i.Description)
+}
diff --git a/rosetta/failure/invalid_key.go b/rosetta/failure/invalid_key.go
new file mode 100755
index 0000000..06e8043
--- /dev/null
+++ b/rosetta/failure/invalid_key.go
@@ -0,0 +1,34 @@
+// Copyright 2021 Optakt Labs OÜ
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+package failure
+
+import (
+ "fmt"
+
+ "github.com/onflow/flow-go/model/flow"
+)
+
+// InvalidKey is the error for an invalid signer key.
+type InvalidKey struct {
+ Description Description
+ Height uint64
+ Address flow.Address
+ Index int
+}
+
+// Error implements the error interface.
+func (i InvalidKey) Error() string {
+ return fmt.Sprintf("invalid signer key (height: %d, address: %s, key index: %d): %s", i.Height, i.Address.Hex(), i.Index, i.Description)
+}
diff --git a/rosetta/failure/invalid_network.go b/rosetta/failure/invalid_network.go
new file mode 100755
index 0000000..3dcb3a0
--- /dev/null
+++ b/rosetta/failure/invalid_network.go
@@ -0,0 +1,31 @@
+// Copyright 2021 Optakt Labs OÜ
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+package failure
+
+import (
+ "fmt"
+)
+
+// InvalidNetwork is the error for an invalid network identifier.
+type InvalidNetwork struct {
+ Description Description
+ HaveNetwork string
+ WantNetwork string
+}
+
+// Error implements the error interface.
+func (i InvalidNetwork) Error() string {
+ return fmt.Sprintf("invalid network (have: %s, want: %s): %s", i.HaveNetwork, i.WantNetwork, i.Description)
+}
diff --git a/rosetta/failure/invalid_operations.go b/rosetta/failure/invalid_operations.go
new file mode 100755
index 0000000..3b6febd
--- /dev/null
+++ b/rosetta/failure/invalid_operations.go
@@ -0,0 +1,31 @@
+// Copyright 2021 Optakt Labs OÜ
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+package failure
+
+import (
+ "fmt"
+)
+
+// InvalidOperations is the error for an invalid set of operations.
+type InvalidOperations struct {
+ Description Description
+ Want uint
+ Have uint
+}
+
+// Error implements the error interface.
+func (i InvalidOperations) Error() string {
+ return fmt.Sprintf("invalid operations (want: %d, have: %d): %s", i.Want, i.Have, i.Description)
+}
diff --git a/rosetta/failure/invalid_payer.go b/rosetta/failure/invalid_payer.go
new file mode 100755
index 0000000..83bd90d
--- /dev/null
+++ b/rosetta/failure/invalid_payer.go
@@ -0,0 +1,33 @@
+// Copyright 2021 Optakt Labs OÜ
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+package failure
+
+import (
+ "fmt"
+
+ "github.com/onflow/flow-go/model/flow"
+)
+
+// InvalidPayer is the error for an invalid transaction payer.
+type InvalidPayer struct {
+ Description Description
+ Have flow.Address
+ Want flow.Address
+}
+
+// Error implements the error interface.
+func (i InvalidPayer) Error() string {
+ return fmt.Sprintf("invalid transaction payer (have: %s, want: %s): %s", i.Have.Hex(), i.Want.Hex(), i.Description)
+}
diff --git a/rosetta/failure/invalid_payload.go b/rosetta/failure/invalid_payload.go
new file mode 100755
index 0000000..fb1ef84
--- /dev/null
+++ b/rosetta/failure/invalid_payload.go
@@ -0,0 +1,30 @@
+// Copyright 2021 Optakt Labs OÜ
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+package failure
+
+import (
+ "fmt"
+)
+
+// InvalidPayload is the error for an invalid transaction payload encoding.
+type InvalidPayload struct {
+ Description Description
+ Encoding string
+}
+
+// Error implements the error interface.
+func (i InvalidPayload) Error() string {
+ return fmt.Sprintf("invalid transaction payload (encoding: %s): %s", i.Encoding, i.Description)
+}
diff --git a/rosetta/failure/invalid_proposer.go b/rosetta/failure/invalid_proposer.go
new file mode 100755
index 0000000..0ea591e
--- /dev/null
+++ b/rosetta/failure/invalid_proposer.go
@@ -0,0 +1,33 @@
+// Copyright 2021 Optakt Labs OÜ
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+package failure
+
+import (
+ "fmt"
+
+ "github.com/onflow/flow-go/model/flow"
+)
+
+// InvalidProposer is the error for an invalid transaction proposer.
+type InvalidProposer struct {
+ Description Description
+ Have flow.Address
+ Want flow.Address
+}
+
+// Error implements the error interface.
+func (i InvalidProposer) Error() string {
+ return fmt.Sprintf("invalid transaction proposer (have: %s, want: %s): %s", i.Have.Hex(), i.Want.Hex(), i.Description)
+}
diff --git a/rosetta/failure/invalid_receiver.go b/rosetta/failure/invalid_receiver.go
new file mode 100755
index 0000000..333481f
--- /dev/null
+++ b/rosetta/failure/invalid_receiver.go
@@ -0,0 +1,30 @@
+// Copyright 2021 Optakt Labs OÜ
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+package failure
+
+import (
+ "fmt"
+)
+
+// InvalidReceiver is the error for an invalid transaction receiver address.
+type InvalidReceiver struct {
+ Description Description
+ Receiver string
+}
+
+// Error implements the error interface.
+func (i InvalidReceiver) Error() string {
+ return fmt.Sprintf("invalid transaction receiver (receiver: %s): %s", i.Receiver, i.Description)
+}
diff --git a/rosetta/failure/invalid_script.go b/rosetta/failure/invalid_script.go
new file mode 100755
index 0000000..2c2a3fd
--- /dev/null
+++ b/rosetta/failure/invalid_script.go
@@ -0,0 +1,31 @@
+// Copyright 2021 Optakt Labs OÜ
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+package failure
+
+import (
+ "fmt"
+)
+
+// InvalidScript is the error for an invalid transaction script.
+type InvalidScript struct {
+ Description Description
+ Script string
+}
+
+// Error implements the error interface.
+func (i InvalidScript) Error() string {
+ // We don't want to print the entire script, that would be gigantic.
+ return fmt.Sprintf("invalid transaction script: %s", i.Description)
+}
diff --git a/rosetta/failure/invalid_signature.go b/rosetta/failure/invalid_signature.go
new file mode 100755
index 0000000..3c7ddb2
--- /dev/null
+++ b/rosetta/failure/invalid_signature.go
@@ -0,0 +1,29 @@
+// Copyright 2021 Optakt Labs OÜ
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+package failure
+
+import (
+ "fmt"
+)
+
+// InvalidSignature is the error for an invalid transaction signature.
+type InvalidSignature struct {
+ Description Description
+}
+
+// Error implements the error interface.
+func (i InvalidSignature) Error() string {
+ return fmt.Sprintf("invalid transaction signature: %s", i.Description)
+}
diff --git a/rosetta/failure/invalid_signatures.go b/rosetta/failure/invalid_signatures.go
new file mode 100755
index 0000000..46c5ed3
--- /dev/null
+++ b/rosetta/failure/invalid_signatures.go
@@ -0,0 +1,31 @@
+// Copyright 2021 Optakt Labs OÜ
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+package failure
+
+import (
+ "fmt"
+)
+
+// InvalidSignatures is the error for an invalid set of transaction signatures.
+type InvalidSignatures struct {
+ Description Description
+ Want uint
+ Have uint
+}
+
+// Error implements the error interface.
+func (i InvalidSignatures) Error() string {
+ return fmt.Sprintf("invalid signatures (want: %d, have: %d): %s", i.Want, i.Have, i.Description)
+}
diff --git a/rosetta/failure/invalid_transaction.go b/rosetta/failure/invalid_transaction.go
new file mode 100755
index 0000000..e9fa984
--- /dev/null
+++ b/rosetta/failure/invalid_transaction.go
@@ -0,0 +1,30 @@
+// Copyright 2021 Optakt Labs OÜ
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+package failure
+
+import (
+ "fmt"
+)
+
+// InvalidTransaction is the error for an invalid transaction hash.
+type InvalidTransaction struct {
+ Description Description
+ Hash string
+}
+
+// Error implements the error interface.
+func (i InvalidTransaction) Error() string {
+ return fmt.Sprintf("invalid transaction (transaction: %s): %s", i.Hash, i.Description)
+}
diff --git a/rosetta/failure/invalid_transaction_hash.go b/rosetta/failure/invalid_transaction_hash.go
new file mode 100755
index 0000000..25f1cab
--- /dev/null
+++ b/rosetta/failure/invalid_transaction_hash.go
@@ -0,0 +1,31 @@
+// Copyright 2021 Optakt Labs OÜ
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+package failure
+
+import (
+ "fmt"
+)
+
+// InvalidTransactionHash is the error for an invalid transaction hash length.
+type InvalidTransactionHash struct {
+ Description Description
+ WantLength int
+ HaveLength int
+}
+
+// Error implements the error interface.
+func (i InvalidTransactionHash) Error() string {
+ return fmt.Sprintf("invalid transaction hash length (want: %d, have: %d): %s", i.WantLength, i.HaveLength, i.Description)
+}
diff --git a/rosetta/failure/unknown_block.go b/rosetta/failure/unknown_block.go
new file mode 100755
index 0000000..6accf57
--- /dev/null
+++ b/rosetta/failure/unknown_block.go
@@ -0,0 +1,31 @@
+// Copyright 2021 Optakt Labs OÜ
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+package failure
+
+import (
+ "fmt"
+)
+
+// UnknownBlock is the error for an unknown block identifier.
+type UnknownBlock struct {
+ Description Description
+ Index uint64
+ Hash string
+}
+
+// Error implements the error interface.
+func (u UnknownBlock) Error() string {
+ return fmt.Sprintf("unknown block (index: %d, hash: %s): %s", u.Index, u.Hash, u.Description)
+}
diff --git a/rosetta/failure/unknown_currency.go b/rosetta/failure/unknown_currency.go
new file mode 100755
index 0000000..a31cf95
--- /dev/null
+++ b/rosetta/failure/unknown_currency.go
@@ -0,0 +1,31 @@
+// Copyright 2021 Optakt Labs OÜ
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+package failure
+
+import (
+ "fmt"
+)
+
+// UnknownCurrency is the error for an unknown currency.
+type UnknownCurrency struct {
+ Description Description
+ Symbol string
+ Decimals uint
+}
+
+// Error implements the error interface.
+func (u UnknownCurrency) Error() string {
+ return fmt.Sprintf("unknown currency (symbol: %s, decimals: %d): %s", u.Symbol, u.Decimals, u.Description)
+}
diff --git a/rosetta/failure/unknown_transaction.go b/rosetta/failure/unknown_transaction.go
new file mode 100755
index 0000000..10e3cf2
--- /dev/null
+++ b/rosetta/failure/unknown_transaction.go
@@ -0,0 +1,30 @@
+// Copyright 2021 Optakt Labs OÜ
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+package failure
+
+import (
+ "fmt"
+)
+
+// UnknownTransaction is the error for an unknown transaction identifier.
+type UnknownTransaction struct {
+ Description Description
+ Hash string
+}
+
+// Error implements the error interface.
+func (u UnknownTransaction) Error() string {
+ return fmt.Sprintf("unknown transaction (hash: %s): %s", u.Hash, u.Description)
+}
diff --git a/rosetta/identifier/account.go b/rosetta/identifier/account.go
new file mode 100755
index 0000000..ab2d9a6
--- /dev/null
+++ b/rosetta/identifier/account.go
@@ -0,0 +1,22 @@
+// Copyright 2021 Optakt Labs OÜ
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+package identifier
+
+// Account uniquely identifies an account within a network. No sub-accounts are used
+// in this implementation for now, though they will probably have to be added to support
+// staking on Coinbase in the future.
+type Account struct {
+ Address string `json:"address"`
+}
diff --git a/rosetta/identifier/block.go b/rosetta/identifier/block.go
new file mode 100755
index 0000000..64cb273
--- /dev/null
+++ b/rosetta/identifier/block.go
@@ -0,0 +1,22 @@
+// Copyright 2021 Optakt Labs OÜ
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+package identifier
+
+// Block uniquely identifies a block in a particular network. As the view is not
+// unique between sporks, index refers to the block height.
+type Block struct {
+ Index *uint64 `json:"index,omitempty"`
+ Hash string `json:"hash,omitempty"`
+}
diff --git a/rosetta/identifier/currency.go b/rosetta/identifier/currency.go
new file mode 100755
index 0000000..2b3bee4
--- /dev/null
+++ b/rosetta/identifier/currency.go
@@ -0,0 +1,27 @@
+// Copyright 2021 Optakt Labs OÜ
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+package identifier
+
+// Currency is composed of a canonical symbol and decimals. The `decimals` value
+// is used to convert an amount value from atomic units (such as satoshis) to
+// standard units (such as bitcoins). As monetary values in Flow are provided as
+// an unsigned fixed point value with 8 decimals, simply use the full integer
+// with 8 decimals in the currency struct. The symbol is always `FLOW`.
+//
+// An example of metadata given in the Rosetta API documentation is `Issuer`.
+type Currency struct {
+ Symbol string `json:"symbol"`
+ Decimals uint `json:"decimals,omitempty"`
+}
diff --git a/rosetta/identifier/network.go b/rosetta/identifier/network.go
new file mode 100755
index 0000000..4bb68bd
--- /dev/null
+++ b/rosetta/identifier/network.go
@@ -0,0 +1,27 @@
+// Copyright 2021 Optakt Labs OÜ
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+package identifier
+
+// Network specifies which network a particular object is associated with. The
+// blockchain field is always set to `flow` and the network is always set to
+// `mainnet`.
+//
+// We are omitting the `SubNetwork` field for now, but we could use it in the
+// future to distinguish between the networks of different sporks (i.e.
+// `candidate4` or `mainnet-5`).
+type Network struct {
+ Blockchain string `json:"blockchain"`
+ Network string `json:"network"`
+}
diff --git a/rosetta/identifier/operation.go b/rosetta/identifier/operation.go
new file mode 100755
index 0000000..b581688
--- /dev/null
+++ b/rosetta/identifier/operation.go
@@ -0,0 +1,22 @@
+// Copyright 2021 Optakt Labs OÜ
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+package identifier
+
+// Operation uniquely identifies an operation within a transaction. No network index is
+// needed because of the absence Flow does not support sharding.
+type Operation struct {
+ Index uint `json:"index"`
+ NetworkIndex *uint `json:"network_index,omitempty"`
+}
diff --git a/rosetta/identifier/transaction.go b/rosetta/identifier/transaction.go
new file mode 100755
index 0000000..3c7be52
--- /dev/null
+++ b/rosetta/identifier/transaction.go
@@ -0,0 +1,21 @@
+// Copyright 2021 Optakt Labs OÜ
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+package identifier
+
+// Transaction uniquely identifies a transaction in a particular network and
+// block.
+type Transaction struct {
+ Hash string `json:"hash"`
+}
diff --git a/rosetta/meta/error_definition.go b/rosetta/meta/error_definition.go
new file mode 100755
index 0000000..4863fc9
--- /dev/null
+++ b/rosetta/meta/error_definition.go
@@ -0,0 +1,22 @@
+// Copyright 2021 Optakt Labs OÜ
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+package meta
+
+// ErrorDefinition is a Rosetta error's definition.
+type ErrorDefinition struct {
+ Code uint `json:"code"`
+ Message string `json:"message"`
+ Retriable bool `json:"retriable"`
+}
diff --git a/rosetta/meta/operation_definition.go b/rosetta/meta/operation_definition.go
new file mode 100755
index 0000000..b011ebc
--- /dev/null
+++ b/rosetta/meta/operation_definition.go
@@ -0,0 +1,21 @@
+// Copyright 2021 Optakt Labs OÜ
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+package meta
+
+// StatusDefinition is a Rosetta operation's status definition.
+type StatusDefinition struct {
+ Status string `json:"status"`
+ Successful bool `json:"successful"`
+}
diff --git a/rosetta/meta/version.go b/rosetta/meta/version.go
new file mode 100755
index 0000000..22f7a32
--- /dev/null
+++ b/rosetta/meta/version.go
@@ -0,0 +1,22 @@
+// Copyright 2021 Optakt Labs OÜ
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+package meta
+
+// Version is the version information of the DPS Rosetta API.
+type Version struct {
+ RosettaVersion string `json:"rosetta_version"`
+ NodeVersion string `json:"node_version"`
+ MiddlewareVersion string `json:"middleware_version"`
+}
diff --git a/rosetta/object/amount.go b/rosetta/object/amount.go
new file mode 100755
index 0000000..537403a
--- /dev/null
+++ b/rosetta/object/amount.go
@@ -0,0 +1,25 @@
+// Copyright 2021 Optakt Labs OÜ
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+package object
+
+import (
+ "github.com/optakt/flow-rosetta/rosetta/identifier"
+)
+
+// Amount is some value of a currency. An amount must have both a value and a currency.
+type Amount struct {
+ Value string `json:"value"`
+ Currency identifier.Currency `json:"currency"`
+}
diff --git a/rosetta/object/block.go b/rosetta/object/block.go
new file mode 100755
index 0000000..a4702c7
--- /dev/null
+++ b/rosetta/object/block.go
@@ -0,0 +1,34 @@
+// Copyright 2021 Optakt Labs OÜ
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+package object
+
+import (
+ "github.com/optakt/flow-rosetta/rosetta/identifier"
+)
+
+// Block contains an array of transactions that occurred at a particular block
+// identifier. A hard requirement for blocks returned by Rosetta implementations
+// is that they must be unalterable: once a client has requested and received a
+// block identified by a specific block identifier, all future calls for that
+// same block identifier must return the same block contents.
+//
+// Examples given of metadata in the Rosetta API documentation are
+// `transaction_root` and `difficulty`.
+type Block struct {
+ ID identifier.Block `json:"block_identifier"`
+ ParentID identifier.Block `json:"parent_block_identifier"`
+ Timestamp int64 `json:"timestamp"`
+ Transactions []*Transaction `json:"transactions"`
+}
diff --git a/rosetta/object/metadata.go b/rosetta/object/metadata.go
new file mode 100755
index 0000000..3836403
--- /dev/null
+++ b/rosetta/object/metadata.go
@@ -0,0 +1,25 @@
+// Copyright 2021 Optakt Labs OÜ
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+package object
+
+import (
+ "github.com/optakt/flow-rosetta/rosetta/identifier"
+)
+
+// Metadata is the information required to construct a transaction for a specific network.
+type Metadata struct {
+ CurrentBlockID identifier.Block `json:"current_block"`
+ SequenceNumber uint64 `json:"sequence_number"`
+}
diff --git a/rosetta/object/operation.go b/rosetta/object/operation.go
new file mode 100755
index 0000000..e673d44
--- /dev/null
+++ b/rosetta/object/operation.go
@@ -0,0 +1,39 @@
+// Copyright 2021 Optakt Labs OÜ
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+package object
+
+import (
+ "github.com/optakt/flow-rosetta/rosetta/identifier"
+)
+
+// Operation contains all balance-changing information within a transaction. It
+// is always one-sided (only affects one account identifier) and can succeed or
+// fail independently of a transaction. Operations are used both to represent
+// on-chain data in the Data API and to construct new transaction in the
+// Construction API, creating a standard interface for reading and writing to
+// blockchains.
+//
+// Examples of metadata given in the Rosetta API documentation are
+// "asm" and "hex".
+//
+// The `coin_change` field is omitted, as the Flow blockchain is an
+// account-based blockchain without utxo set.
+type Operation struct {
+ ID identifier.Operation `json:"operation_identifier"`
+ Type string `json:"type"`
+ Status string `json:"status,omitempty"`
+ AccountID identifier.Account `json:"account"`
+ Amount Amount `json:"amount"`
+}
diff --git a/rosetta/object/options.go b/rosetta/object/options.go
new file mode 100755
index 0000000..dd28051
--- /dev/null
+++ b/rosetta/object/options.go
@@ -0,0 +1,31 @@
+// Copyright 2021 Optakt Labs OÜ
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+package object
+
+import (
+ "github.com/optakt/flow-rosetta/rosetta/identifier"
+)
+
+// Options object is used in the Rosetta Construction API requests.
+// This object is returned in the `/construction/preprocess` response,
+// and is forwarded to the `/construction/metadata` endpoint unmodified.
+//
+// Specifically for Flow DPS, this object contains the account identifier
+// that is the proposer of the transaction (by default, this is the sender).
+// Account identifier is required so that we can return the sequence number
+// of the proposer's key, required for the Flow transaction.
+type Options struct {
+ AccountID identifier.Account `json:"account_identifier"`
+}
diff --git a/rosetta/object/signature.go b/rosetta/object/signature.go
new file mode 100755
index 0000000..6b7b849
--- /dev/null
+++ b/rosetta/object/signature.go
@@ -0,0 +1,29 @@
+// Copyright 2021 Optakt Labs OÜ
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+package object
+
+// Signature contains the information about a transaction signature.
+type Signature struct {
+ SigningPayload SigningPayload `json:"signing_payload"`
+ SignatureType string `json:"signature_type"`
+ HexBytes string `json:"hex_bytes"`
+ PublicKey PublicKey `json:"public_key"`
+}
+
+// PublicKey represents a public key used in a transaction signature.
+type PublicKey struct {
+ HexBytes string `json:"hex_bytes"`
+ CurveType string `json:"curve_type"`
+}
diff --git a/rosetta/object/signing_payload.go b/rosetta/object/signing_payload.go
new file mode 100755
index 0000000..1cf4702
--- /dev/null
+++ b/rosetta/object/signing_payload.go
@@ -0,0 +1,26 @@
+// Copyright 2021 Optakt Labs OÜ
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+package object
+
+import (
+ "github.com/optakt/flow-rosetta/rosetta/identifier"
+)
+
+// SigningPayload is the payload of an account's signature.
+type SigningPayload struct {
+ AccountID identifier.Account `json:"account_identifier"`
+ HexBytes string `json:"hex_bytes"`
+ SignatureType string `json:"signature_type"`
+}
diff --git a/rosetta/object/transaction.go b/rosetta/object/transaction.go
new file mode 100755
index 0000000..141b244
--- /dev/null
+++ b/rosetta/object/transaction.go
@@ -0,0 +1,29 @@
+// Copyright 2021 Optakt Labs OÜ
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+package object
+
+import (
+ "github.com/optakt/flow-rosetta/rosetta/identifier"
+)
+
+// Transaction contains an array of operations that are attributable to the same
+// transaction identifier.
+//
+// Examples of metadata given in the Rosetta API documentation are "size" and
+// "lockTime".
+type Transaction struct {
+ ID identifier.Transaction `json:"transaction_identifier"`
+ Operations []*Operation `json:"operations"`
+}
diff --git a/rosetta/request/balance.go b/rosetta/request/balance.go
new file mode 100755
index 0000000..59d105c
--- /dev/null
+++ b/rosetta/request/balance.go
@@ -0,0 +1,28 @@
+// Copyright 2021 Optakt Labs OÜ
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+package request
+
+import (
+ "github.com/optakt/flow-rosetta/rosetta/identifier"
+)
+
+// Balance implements the request schema for /account/balance.
+// See https://www.rosetta-api.org/docs/AccountApi.html#request
+type Balance struct {
+ NetworkID identifier.Network `json:"network_identifier"`
+ BlockID identifier.Block `json:"block_identifier"`
+ AccountID identifier.Account `json:"account_identifier"`
+ Currencies []identifier.Currency `json:"currencies"`
+}
diff --git a/rosetta/request/block.go b/rosetta/request/block.go
new file mode 100755
index 0000000..faef4d6
--- /dev/null
+++ b/rosetta/request/block.go
@@ -0,0 +1,26 @@
+// Copyright 2021 Optakt Labs OÜ
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+package request
+
+import (
+ "github.com/optakt/flow-rosetta/rosetta/identifier"
+)
+
+// Block implements the request schema for /block.
+// See https://www.rosetta-api.org/docs/BlockApi.html#request
+type Block struct {
+ NetworkID identifier.Network `json:"network_identifier"`
+ BlockID identifier.Block `json:"block_identifier"`
+}
diff --git a/rosetta/request/combine.go b/rosetta/request/combine.go
new file mode 100755
index 0000000..ab7bba7
--- /dev/null
+++ b/rosetta/request/combine.go
@@ -0,0 +1,28 @@
+// Copyright 2021 Optakt Labs OÜ
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+package request
+
+import (
+ "github.com/optakt/flow-rosetta/rosetta/identifier"
+ "github.com/optakt/flow-rosetta/rosetta/object"
+)
+
+// Combine implements the request schema for /construction/combine.
+// See https://www.rosetta-api.org/docs/ConstructionApi.html#request
+type Combine struct {
+ NetworkID identifier.Network `json:"network_identifier"`
+ UnsignedTransaction string `json:"unsigned_transaction"`
+ Signatures []object.Signature `json:"signatures"`
+}
diff --git a/rosetta/request/hash.go b/rosetta/request/hash.go
new file mode 100755
index 0000000..ef6d800
--- /dev/null
+++ b/rosetta/request/hash.go
@@ -0,0 +1,26 @@
+// Copyright 2021 Optakt Labs OÜ
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+package request
+
+import (
+ "github.com/optakt/flow-rosetta/rosetta/identifier"
+)
+
+// Hash implements the request schema for /construction/hash.
+// See https://www.rosetta-api.org/docs/ConstructionApi.html#request-2
+type Hash struct {
+ NetworkID identifier.Network `json:"network_identifier"`
+ SignedTransaction string `json:"signed_transaction"`
+}
diff --git a/rosetta/request/metadata.go b/rosetta/request/metadata.go
new file mode 100755
index 0000000..06351a0
--- /dev/null
+++ b/rosetta/request/metadata.go
@@ -0,0 +1,29 @@
+// Copyright 2021 Optakt Labs OÜ
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+package request
+
+import (
+ "github.com/optakt/flow-rosetta/rosetta/identifier"
+ "github.com/optakt/flow-rosetta/rosetta/object"
+)
+
+// Metadata implements the request schema for /construction/metadata.
+// `Options` object in this request is generated by a call to `/construction/preprocess`,
+// and should be sent unaltered as returned by that endpoint.
+// See https://www.rosetta-api.org/docs/ConstructionApi.html#request-3
+type Metadata struct {
+ NetworkID identifier.Network `json:"network_identifier"`
+ Options object.Options `json:"options"`
+}
diff --git a/rosetta/request/networks.go b/rosetta/request/networks.go
new file mode 100755
index 0000000..03fb32b
--- /dev/null
+++ b/rosetta/request/networks.go
@@ -0,0 +1,19 @@
+// Copyright 2021 Optakt Labs OÜ
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+package request
+
+// Networks implements the empty request schema for the /network/list endpoint.
+// See https://www.rosetta-api.org/docs/NetworkApi.html#request
+type Networks struct{}
diff --git a/rosetta/request/options.go b/rosetta/request/options.go
new file mode 100755
index 0000000..b87164b
--- /dev/null
+++ b/rosetta/request/options.go
@@ -0,0 +1,25 @@
+// Copyright 2021 Optakt Labs OÜ
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+package request
+
+import (
+ "github.com/optakt/flow-rosetta/rosetta/identifier"
+)
+
+// Options implements the empty request schema for the /network/options endpoint.
+// See https://www.rosetta-api.org/docs/NetworkApi.html#request-1
+type Options struct {
+ NetworkID identifier.Network `json:"network_identifier"`
+}
diff --git a/rosetta/request/parse.go b/rosetta/request/parse.go
new file mode 100755
index 0000000..3c25259
--- /dev/null
+++ b/rosetta/request/parse.go
@@ -0,0 +1,27 @@
+// Copyright 2021 Optakt Labs OÜ
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+package request
+
+import (
+ "github.com/optakt/flow-rosetta/rosetta/identifier"
+)
+
+// Parse implements the request schema for /construction/parse.
+// See https://www.rosetta-api.org/docs/ConstructionApi.html#request-4
+type Parse struct {
+ NetworkID identifier.Network `json:"network_identifier"`
+ Signed bool `json:"signed"`
+ Transaction string `json:"transaction"`
+}
diff --git a/rosetta/request/payloads.go b/rosetta/request/payloads.go
new file mode 100755
index 0000000..c064511
--- /dev/null
+++ b/rosetta/request/payloads.go
@@ -0,0 +1,28 @@
+// Copyright 2021 Optakt Labs OÜ
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+package request
+
+import (
+ "github.com/optakt/flow-rosetta/rosetta/identifier"
+ "github.com/optakt/flow-rosetta/rosetta/object"
+)
+
+// Payloads implements the request schema for /construction/payloads.
+// See https://www.rosetta-api.org/docs/ConstructionApi.html#request-5
+type Payloads struct {
+ NetworkID identifier.Network `json:"network_identifier"`
+ Operations []object.Operation `json:"operations"`
+ Metadata object.Metadata `json:"metadata"`
+}
diff --git a/rosetta/request/preprocess.go b/rosetta/request/preprocess.go
new file mode 100755
index 0000000..2af5a0f
--- /dev/null
+++ b/rosetta/request/preprocess.go
@@ -0,0 +1,27 @@
+// Copyright 2021 Optakt Labs OÜ
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+package request
+
+import (
+ "github.com/optakt/flow-rosetta/rosetta/identifier"
+ "github.com/optakt/flow-rosetta/rosetta/object"
+)
+
+// Preprocess implements the request schema for /construction/preprocess.
+// See https://www.rosetta-api.org/docs/ConstructionApi.html#request-6
+type Preprocess struct {
+ NetworkID identifier.Network `json:"network_identifier"`
+ Operations []object.Operation `json:"operations"`
+}
diff --git a/rosetta/request/status.go b/rosetta/request/status.go
new file mode 100755
index 0000000..3f0d1cc
--- /dev/null
+++ b/rosetta/request/status.go
@@ -0,0 +1,25 @@
+// Copyright 2021 Optakt Labs OÜ
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+package request
+
+import (
+ "github.com/optakt/flow-rosetta/rosetta/identifier"
+)
+
+// Status implements the request schema for /network/status.
+// See https://www.rosetta-api.org/docs/NetworkApi.html#request-2
+type Status struct {
+ NetworkID identifier.Network `json:"network_identifier"`
+}
diff --git a/rosetta/request/submit.go b/rosetta/request/submit.go
new file mode 100755
index 0000000..91b4020
--- /dev/null
+++ b/rosetta/request/submit.go
@@ -0,0 +1,26 @@
+// Copyright 2021 Optakt Labs OÜ
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+package request
+
+import (
+ "github.com/optakt/flow-rosetta/rosetta/identifier"
+)
+
+// Submit implements the request schema for /construction/submit.
+// See https://www.rosetta-api.org/docs/ConstructionApi.html#request-7
+type Submit struct {
+ NetworkID identifier.Network `json:"network_identifier"`
+ SignedTransaction string `json:"signed_transaction"`
+}
diff --git a/rosetta/request/transaction.go b/rosetta/request/transaction.go
new file mode 100755
index 0000000..7695884
--- /dev/null
+++ b/rosetta/request/transaction.go
@@ -0,0 +1,27 @@
+// Copyright 2021 Optakt Labs OÜ
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+package request
+
+import (
+ "github.com/optakt/flow-rosetta/rosetta/identifier"
+)
+
+// Transaction implements the request schema for /block/transaction.
+// See https://www.rosetta-api.org/docs/BlockApi.html#request-1
+type Transaction struct {
+ NetworkID identifier.Network `json:"network_identifier"`
+ BlockID identifier.Block `json:"block_identifier"`
+ TransactionID identifier.Transaction `json:"transaction_identifier"`
+}
diff --git a/rosetta/response/balance.go b/rosetta/response/balance.go
new file mode 100755
index 0000000..440a4b7
--- /dev/null
+++ b/rosetta/response/balance.go
@@ -0,0 +1,27 @@
+// Copyright 2021 Optakt Labs OÜ
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+package response
+
+import (
+ "github.com/optakt/flow-rosetta/rosetta/identifier"
+ "github.com/optakt/flow-rosetta/rosetta/object"
+)
+
+// Balance implements the successful response schema for /account/balance.
+// See https://www.rosetta-api.org/docs/AccountApi.html#200---ok
+type Balance struct {
+ BlockID identifier.Block `json:"block_identifier"`
+ Balances []object.Amount `json:"balances"`
+}
diff --git a/rosetta/response/block.go b/rosetta/response/block.go
new file mode 100755
index 0000000..9b9e3ca
--- /dev/null
+++ b/rosetta/response/block.go
@@ -0,0 +1,27 @@
+// Copyright 2021 Optakt Labs OÜ
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+package response
+
+import (
+ "github.com/optakt/flow-rosetta/rosetta/identifier"
+ "github.com/optakt/flow-rosetta/rosetta/object"
+)
+
+// Block implements the response schema for /block.
+// See https://www.rosetta-api.org/docs/BlockApi.html#200---ok
+type Block struct {
+ Block *object.Block `json:"block"`
+ OtherTransactions []identifier.Transaction `json:"other_transactions,omitempty"`
+}
diff --git a/rosetta/response/combine.go b/rosetta/response/combine.go
new file mode 100755
index 0000000..f36b412
--- /dev/null
+++ b/rosetta/response/combine.go
@@ -0,0 +1,21 @@
+// Copyright 2021 Optakt Labs OÜ
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+package response
+
+// Combine implements the response schema for /construction/combine.
+// See https://www.rosetta-api.org/docs/ConstructionApi.html#response
+type Combine struct {
+ SignedTransaction string `json:"signed_transaction"`
+}
diff --git a/rosetta/response/hash.go b/rosetta/response/hash.go
new file mode 100755
index 0000000..32adc94
--- /dev/null
+++ b/rosetta/response/hash.go
@@ -0,0 +1,25 @@
+// Copyright 2021 Optakt Labs OÜ
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+package response
+
+import (
+ "github.com/optakt/flow-rosetta/rosetta/identifier"
+)
+
+// Hash implements the response schema for /construction/hash.
+// See https://www.rosetta-api.org/docs/ConstructionApi.html#response-2
+type Hash struct {
+ TransactionID identifier.Transaction `json:"transaction_identifier"`
+}
diff --git a/rosetta/response/metadata.go b/rosetta/response/metadata.go
new file mode 100755
index 0000000..80b2adf
--- /dev/null
+++ b/rosetta/response/metadata.go
@@ -0,0 +1,25 @@
+// Copyright 2021 Optakt Labs OÜ
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+package response
+
+import (
+ "github.com/optakt/flow-rosetta/rosetta/object"
+)
+
+// Metadata implements the response schema for /construction/metadata.
+// See https://www.rosetta-api.org/docs/ConstructionApi.html#response-3
+type Metadata struct {
+ Metadata object.Metadata `json:"metadata"`
+}
diff --git a/rosetta/response/networks.go b/rosetta/response/networks.go
new file mode 100755
index 0000000..de9101e
--- /dev/null
+++ b/rosetta/response/networks.go
@@ -0,0 +1,25 @@
+// Copyright 2021 Optakt Labs OÜ
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+package response
+
+import (
+ "github.com/optakt/flow-rosetta/rosetta/identifier"
+)
+
+// Networks implements the successful response schema for the /network/list endpoint.
+// See https://www.rosetta-api.org/docs/NetworkApi.html#200---ok
+type Networks struct {
+ NetworkIDs []identifier.Network `json:"network_identifiers"`
+}
diff --git a/rosetta/response/options.go b/rosetta/response/options.go
new file mode 100755
index 0000000..d3d1f88
--- /dev/null
+++ b/rosetta/response/options.go
@@ -0,0 +1,38 @@
+// Copyright 2021 Optakt Labs OÜ
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+package response
+
+import (
+ "github.com/optakt/flow-rosetta/rosetta/meta"
+)
+
+// Options implements the successful response schema for the /network/options endpoint.
+// See https://www.rosetta-api.org/docs/NetworkApi.html#200---ok-1
+type Options struct {
+ Version meta.Version `json:"version"`
+ Allow OptionsAllow `json:"allow"`
+}
+
+// OptionsAllow specifies supported Operation statuses, Operation types, and all possible
+// error statuses. It is returned by the /network/options endpoint.
+type OptionsAllow struct {
+ OperationStatuses []meta.StatusDefinition `json:"operation_statuses"`
+ OperationTypes []string `json:"operation_types"`
+ Errors []meta.ErrorDefinition `json:"errors"`
+ HistoricalBalanceLookup bool `json:"historical_balance_lookup"`
+ CallMethods []string `json:"call_methods"` // not used
+ BalanceExemptions []struct{} `json:"balance_exemptions"` // not used
+ MempoolCoins bool `json:"mempool_coins"`
+}
diff --git a/rosetta/response/parse.go b/rosetta/response/parse.go
new file mode 100755
index 0000000..a8e3df9
--- /dev/null
+++ b/rosetta/response/parse.go
@@ -0,0 +1,28 @@
+// Copyright 2021 Optakt Labs OÜ
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+package response
+
+import (
+ "github.com/optakt/flow-rosetta/rosetta/identifier"
+ "github.com/optakt/flow-rosetta/rosetta/object"
+)
+
+// Parse implements the response schema for /construction/parse.
+// See https://www.rosetta-api.org/docs/ConstructionApi.html#response-4
+type Parse struct {
+ Operations []object.Operation `json:"operations"`
+ SignerIDs []identifier.Account `json:"account_identifier_signers,omitempty"`
+ Metadata object.Metadata `json:"metadata,omitempty"`
+}
diff --git a/rosetta/response/payloads.go b/rosetta/response/payloads.go
new file mode 100755
index 0000000..b9a21b5
--- /dev/null
+++ b/rosetta/response/payloads.go
@@ -0,0 +1,26 @@
+// Copyright 2021 Optakt Labs OÜ
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+package response
+
+import (
+ "github.com/optakt/flow-rosetta/rosetta/object"
+)
+
+// Payloads implements the response schema for /construction/payloads.
+// See https://www.rosetta-api.org/docs/ConstructionApi.html#response-5
+type Payloads struct {
+ Transaction string `json:"unsigned_transaction"`
+ Payloads []object.SigningPayload `json:"payloads"`
+}
diff --git a/rosetta/response/preprocess.go b/rosetta/response/preprocess.go
new file mode 100755
index 0000000..bf0a52c
--- /dev/null
+++ b/rosetta/response/preprocess.go
@@ -0,0 +1,25 @@
+// Copyright 2021 Optakt Labs OÜ
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+package response
+
+import (
+ "github.com/optakt/flow-rosetta/rosetta/object"
+)
+
+// Preprocess implements the response schema for /construction/preprocess.
+// See https://www.rosetta-api.org/docs/ConstructionApi.html#response-6
+type Preprocess struct {
+ object.Options `json:"options,omitempty"`
+}
diff --git a/rosetta/response/status.go b/rosetta/response/status.go
new file mode 100755
index 0000000..a22d78b
--- /dev/null
+++ b/rosetta/response/status.go
@@ -0,0 +1,29 @@
+// Copyright 2021 Optakt Labs OÜ
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+package response
+
+import (
+ "github.com/optakt/flow-rosetta/rosetta/identifier"
+)
+
+// Status implements the successful response schema for /network/status.
+// See https://www.rosetta-api.org/docs/NetworkApi.html#200---ok-2
+type Status struct {
+ CurrentBlockID identifier.Block `json:"current_block_identifier"`
+ CurrentBlockTimestamp int64 `json:"current_block_timestamp"`
+ OldestBlockID identifier.Block `json:"oldest_block_identifier"`
+ GenesisBlockID identifier.Block `json:"genesis_block_identifier"`
+ Peers []struct{} `json:"peers"` // not used
+}
diff --git a/rosetta/response/submit.go b/rosetta/response/submit.go
new file mode 100755
index 0000000..2560c0c
--- /dev/null
+++ b/rosetta/response/submit.go
@@ -0,0 +1,25 @@
+// Copyright 2021 Optakt Labs OÜ
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+package response
+
+import (
+ "github.com/optakt/flow-rosetta/rosetta/identifier"
+)
+
+// Submit implements the response schema for /construction/submit.
+// See https://www.rosetta-api.org/docs/ConstructionApi.html#response-7
+type Submit struct {
+ TransactionID identifier.Transaction `json:"transaction_identifier"`
+}
diff --git a/rosetta/response/transaction.go b/rosetta/response/transaction.go
new file mode 100755
index 0000000..4bb659a
--- /dev/null
+++ b/rosetta/response/transaction.go
@@ -0,0 +1,25 @@
+// Copyright 2021 Optakt Labs OÜ
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+package response
+
+import (
+ "github.com/optakt/flow-rosetta/rosetta/object"
+)
+
+// Transaction implements the successful response schema for /block/transaction.
+// See https://www.rosetta-api.org/docs/BlockApi.html#200---ok-1
+type Transaction struct {
+ Transaction *object.Transaction `json:"transaction"`
+}
diff --git a/rosetta/retriever/config.go b/rosetta/retriever/config.go
new file mode 100755
index 0000000..927b4ac
--- /dev/null
+++ b/rosetta/retriever/config.go
@@ -0,0 +1,27 @@
+// Copyright 2021 Optakt Labs OÜ
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+package retriever
+
+// Config is the configuration for the Rosetta retriever component.
+type Config struct {
+ TransactionLimit uint
+}
+
+// WithTransactionLimit sets a transaction limit in a Config.
+func WithTransactionLimit(limit uint) func(*Config) {
+ return func(c *Config) {
+ c.TransactionLimit = limit
+ }
+}
diff --git a/rosetta/retriever/convert.go b/rosetta/retriever/convert.go
new file mode 100755
index 0000000..0a4aac5
--- /dev/null
+++ b/rosetta/retriever/convert.go
@@ -0,0 +1,41 @@
+// Copyright 2021 Optakt Labs OÜ
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+package retriever
+
+import (
+ "github.com/onflow/flow-go/model/flow"
+
+ "github.com/optakt/flow-rosetta/rosetta/identifier"
+)
+
+func rosettaTxID(txID flow.Identifier) identifier.Transaction {
+ return identifier.Transaction{
+ Hash: txID.String(),
+ }
+}
+
+func rosettaBlockID(height uint64, blockID flow.Identifier) identifier.Block {
+ return identifier.Block{
+ Index: &height,
+ Hash: blockID.String(),
+ }
+}
+
+func rosettaCurrency(symbol string, decimals uint) identifier.Currency {
+ return identifier.Currency{
+ Symbol: symbol,
+ Decimals: decimals,
+ }
+}
diff --git a/rosetta/retriever/converter.go b/rosetta/retriever/converter.go
new file mode 100755
index 0000000..0ef7a19
--- /dev/null
+++ b/rosetta/retriever/converter.go
@@ -0,0 +1,26 @@
+// Copyright 2021 Optakt Labs OÜ
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+package retriever
+
+import (
+ "github.com/onflow/flow-go/model/flow"
+
+ "github.com/optakt/flow-rosetta/rosetta/object"
+)
+
+// Converter represents something that can convert Flow events into Rosetta operations.
+type Converter interface {
+ EventToOperation(event flow.Event) (operation *object.Operation, err error)
+}
diff --git a/rosetta/retriever/errors.go b/rosetta/retriever/errors.go
new file mode 100755
index 0000000..9092d1f
--- /dev/null
+++ b/rosetta/retriever/errors.go
@@ -0,0 +1,20 @@
+package retriever
+
+import (
+ "errors"
+)
+
+// Rosetta Sentinel Errors.
+var (
+ ErrNoAddress = errors.New("event without address")
+ ErrNotSupported = errors.New("unsupported event type")
+)
+
+const (
+ // Cadence error returned when it was not possible to borrow the vault reference.
+ // This can happen if the account does not exist at the given height.
+ missingVault = "Could not borrow Balance reference to the Vault"
+
+ // Error description for failure to find a transaction.
+ txMissing = "transaction not found in given block"
+)
diff --git a/rosetta/retriever/generator.go b/rosetta/retriever/generator.go
new file mode 100755
index 0000000..1d0b2df
--- /dev/null
+++ b/rosetta/retriever/generator.go
@@ -0,0 +1,23 @@
+// Copyright 2021 Optakt Labs OÜ
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+package retriever
+
+// Generator represents something that can generate scripts for retrieving
+// balances as well as the amounts deposited and withdrawn for a given token.
+type Generator interface {
+ GetBalance(symbol string) ([]byte, error)
+ TokensDeposited(symbol string) (string, error)
+ TokensWithdrawn(symbol string) (string, error)
+}
diff --git a/rosetta/retriever/invoker.go b/rosetta/retriever/invoker.go
new file mode 100755
index 0000000..b7ab809
--- /dev/null
+++ b/rosetta/retriever/invoker.go
@@ -0,0 +1,27 @@
+// Copyright 2021 Optakt Labs OÜ
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+package retriever
+
+import (
+ "github.com/onflow/cadence"
+ "github.com/onflow/flow-go/model/flow"
+)
+
+// Invoker represents something that can retrieve public keys at any given height, and
+// execute scripts to retrieve values from the Flow Virtual Machine.
+type Invoker interface {
+ Key(height uint64, address flow.Address, index int) (*flow.AccountPublicKey, error)
+ Script(height uint64, script []byte, parameters []cadence.Value) (cadence.Value, error)
+}
diff --git a/rosetta/retriever/retriever.go b/rosetta/retriever/retriever.go
new file mode 100755
index 0000000..3a99ed3
--- /dev/null
+++ b/rosetta/retriever/retriever.go
@@ -0,0 +1,424 @@
+// Copyright 2021 Optakt Labs OÜ
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+package retriever
+
+import (
+ "errors"
+ "fmt"
+ "sort"
+ "strconv"
+ "strings"
+ "time"
+
+ "github.com/onflow/cadence"
+ "github.com/onflow/flow-go/model/flow"
+
+ "github.com/optakt/flow-dps/models/dps"
+ "github.com/optakt/flow-rosetta/rosetta/failure"
+ "github.com/optakt/flow-rosetta/rosetta/identifier"
+ "github.com/optakt/flow-rosetta/rosetta/object"
+)
+
+// Retriever is a component that retrieves information from the DPS index and converts it into
+// a format that is appropriate for the Rosetta API.
+type Retriever struct {
+ cfg Config
+
+ params dps.Params
+ index dps.Reader
+ validate Validator
+ generate Generator
+ invoke Invoker
+ convert Converter
+}
+
+// New instantiates and returns a Retriever using the injected dependencies, as well as the provided options.
+func New(params dps.Params, index dps.Reader, validate Validator, generator Generator, invoke Invoker, convert Converter, options ...func(*Config)) *Retriever {
+
+ cfg := Config{
+ TransactionLimit: 200,
+ }
+
+ for _, opt := range options {
+ opt(&cfg)
+ }
+
+ r := Retriever{
+ cfg: cfg,
+ params: params,
+ index: index,
+ validate: validate,
+ generate: generator,
+ invoke: invoke,
+ convert: convert,
+ }
+
+ return &r
+}
+
+// Oldest retrieves the oldest block identifier as well as its timestamp.
+func (r *Retriever) Oldest() (identifier.Block, time.Time, error) {
+
+ first, err := r.index.First()
+ if err != nil {
+ return identifier.Block{}, time.Time{}, fmt.Errorf("could not find first indexed block: %w", err)
+ }
+
+ header, err := r.index.Header(first)
+ if err != nil {
+ return identifier.Block{}, time.Time{}, fmt.Errorf("could not find block header: %w", err)
+ }
+
+ block := identifier.Block{
+ Hash: header.ID().String(),
+ Index: &header.Height,
+ }
+
+ return block, header.Timestamp, nil
+}
+
+// Current retrieves the last block identifier as well as its timestamp.
+func (r *Retriever) Current() (identifier.Block, time.Time, error) {
+
+ last, err := r.index.Last()
+ if err != nil {
+ return identifier.Block{}, time.Time{}, fmt.Errorf("could not find last indexed block: %w", err)
+ }
+
+ header, err := r.index.Header(last)
+ if err != nil {
+ return identifier.Block{}, time.Time{}, fmt.Errorf("could not find block header: %w", err)
+ }
+
+ block := identifier.Block{
+ Hash: header.ID().String(),
+ Index: &header.Height,
+ }
+
+ return block, header.Timestamp, nil
+}
+
+// Balances retrieves the balances for the given currencies of the given account ID at the given block.
+func (r *Retriever) Balances(rosBlockID identifier.Block, rosAccountID identifier.Account, rosCurrencies []identifier.Currency) (identifier.Block, []object.Amount, error) {
+
+ // Run validation on the Rosetta block identifier. If it is valid, this will
+ // return the associated Flow block height and block ID.
+ height, blockID, err := r.validate.Block(rosBlockID)
+ if err != nil {
+ return identifier.Block{}, nil, fmt.Errorf("could not validate block: %w", err)
+ }
+
+ // Run validation on the account qualifier. If it is valid, this will return
+ // the associated Flow account address.
+ address, err := r.validate.Account(rosAccountID)
+ if err != nil {
+ return identifier.Block{}, nil, fmt.Errorf("could not validate account: %w", err)
+ }
+
+ // Run validation on the currency qualifiers. For each valid currency, this
+ // will return the associated currency symbol and number of decimals.
+ symbols := make([]string, 0, len(rosCurrencies))
+ decimals := make(map[string]uint, len(rosCurrencies))
+ for _, currency := range rosCurrencies {
+ symbol, decimal, err := r.validate.Currency(currency)
+ if err != nil {
+ return identifier.Block{}, nil, fmt.Errorf("could not validate currency: %w", err)
+ }
+ symbols = append(symbols, symbol)
+ decimals[symbol] = decimal
+ }
+
+ // Get the Cadence value that is the result of the script execution.
+ amounts := make([]object.Amount, 0, len(symbols))
+ for _, symbol := range symbols {
+
+ // We generate the script to get the vault balance and execute it.
+ script, err := r.generate.GetBalance(symbol)
+ if err != nil {
+ return identifier.Block{}, nil, fmt.Errorf("could not generate script: %w", err)
+ }
+ params := []cadence.Value{cadence.NewAddress(address)}
+ result, err := r.invoke.Script(height, script, params)
+ if err != nil && !strings.Contains(err.Error(), missingVault) {
+ return identifier.Block{}, nil, fmt.Errorf("could not invoke script: %w", err)
+ }
+
+ // In the previous error check, we exclude errors that are about getting
+ // the vault reference in Cadence. In those cases, we keep the default
+ // balance here, which is zero.
+ balance := uint64(0)
+ if err == nil {
+ var ok bool
+ balance, ok = result.ToGoValue().(uint64)
+ if !ok {
+ return identifier.Block{}, nil, fmt.Errorf("unexpected script result type (got: %s, want uint64)", result.String())
+ }
+ }
+
+ amount := object.Amount{
+ Currency: rosettaCurrency(symbol, decimals[symbol]),
+ Value: strconv.FormatUint(balance, 10),
+ }
+
+ amounts = append(amounts, amount)
+ }
+
+ return rosettaBlockID(height, blockID), amounts, nil
+}
+
+// Block retrieves a block and its transactions given its identifier.
+func (r *Retriever) Block(rosBlockID identifier.Block) (*object.Block, []identifier.Transaction, error) {
+
+ // Run validation on the Rosetta block identifier. If it is valid, this will
+ // return the associated Flow block height and block ID.
+ height, blockID, err := r.validate.Block(rosBlockID)
+ if err != nil {
+ return nil, nil, fmt.Errorf("could not validate block: %w", err)
+ }
+
+ // Retrieve the Flow token default withdrawal and deposit events.
+ deposit, err := r.generate.TokensDeposited(dps.FlowSymbol)
+ if err != nil {
+ return nil, nil, fmt.Errorf("could not generate deposit event type: %w", err)
+ }
+ withdrawal, err := r.generate.TokensWithdrawn(dps.FlowSymbol)
+ if err != nil {
+ return nil, nil, fmt.Errorf("could not generate withdrawal event type: %w", err)
+ }
+
+ // Then, get the header; it contains the block ID, parent ID and timestamp.
+ header, err := r.index.Header(height)
+ if err != nil {
+ return nil, nil, fmt.Errorf("could not get header: %w", err)
+ }
+
+ // Next, we get all the events for the block to extract deposit and withdrawal events.
+ events, err := r.index.Events(height, flow.EventType(deposit), flow.EventType(withdrawal))
+ if err != nil {
+ return nil, nil, fmt.Errorf("could not get events: %w", err)
+ }
+
+ // Get all transaction IDs for this height.
+ txIDs, err := r.index.TransactionsByHeight(height)
+ if err != nil {
+ return nil, nil, fmt.Errorf("could not get transactions by height: %w", err)
+ }
+
+ // Go over all the transaction IDs and create the related Rosetta transaction
+ // until we hit the limit, at which point we just add the identifier.
+ var blockTransactions []*object.Transaction
+ var extraTransactions []identifier.Transaction
+ for index, txID := range txIDs {
+ if index >= int(r.cfg.TransactionLimit) {
+ extraTransactions = append(extraTransactions, rosettaTxID(txID))
+ continue
+ }
+ ops, err := r.operations(txID, events)
+ if err != nil {
+ return nil, nil, fmt.Errorf("could not get operations: %w", err)
+ }
+ rosTx := object.Transaction{
+ ID: rosettaTxID(txID),
+ Operations: ops,
+ }
+ blockTransactions = append(blockTransactions, &rosTx)
+ }
+
+ // Rosetta spec notes that for genesis block, it is recommended to use the
+ // genesis block identifier also for the parent block identifier.
+ // See https://www.rosetta-api.org/docs/common_mistakes.html#malformed-genesis-block
+ // We thus initialize the parent as the current block, and if the header is
+ // not the root block, we use its actual parent.
+ first, err := r.index.First()
+ if err != nil {
+ return nil, nil, fmt.Errorf("could not get first block index: %w", err)
+ }
+ var parent identifier.Block
+ if header.Height == first {
+ parent = rosettaBlockID(height, blockID)
+ } else {
+ parent = rosettaBlockID(height-1, header.ParentID)
+ }
+
+ // Now we just need to build the block.
+ block := object.Block{
+ ID: rosettaBlockID(height, blockID),
+ ParentID: parent,
+ Timestamp: header.Timestamp.UnixNano() / 1_000_000,
+ Transactions: blockTransactions,
+ }
+
+ return &block, extraTransactions, nil
+}
+
+// Transaction retrieves a transaction given its identifier and the identifier of the block it is a part of.
+func (r *Retriever) Transaction(rosBlockID identifier.Block, rosTxID identifier.Transaction) (*object.Transaction, error) {
+
+ // Run validation on the Rosetta block identifier. If it is valid, this will
+ // return the associated Flow block height and block ID.
+ height, blockID, err := r.validate.Block(rosBlockID)
+ if err != nil {
+ return nil, fmt.Errorf("could not validate block: %w", err)
+ }
+
+ // Run validation on the transaction qualifier. If it is valid, this will return
+ // the associated Flow transaction ID.
+ txID, err := r.validate.Transaction(rosTxID)
+ if err != nil {
+ return nil, fmt.Errorf("could not validate transaction: %w", err)
+ }
+
+ // We retrieve all transaction IDs for the given block height to check that
+ // our transaction is part of it.
+ txIDs, err := r.index.TransactionsByHeight(height)
+ if err != nil {
+ return nil, fmt.Errorf("could not list block transactions: %w", err)
+ }
+ lookup := make(map[flow.Identifier]struct{})
+ for _, txID := range txIDs {
+ lookup[txID] = struct{}{}
+ }
+ _, ok := lookup[txID]
+ if !ok {
+ return nil, failure.UnknownTransaction{
+ Hash: rosTxID.Hash,
+ Description: failure.NewDescription(txMissing,
+ failure.WithUint64("block_index", height),
+ failure.WithID("block_hash", blockID),
+ ),
+ }
+ }
+
+ // Retrieve the Flow token default withdrawal and deposit events.
+ deposit, err := r.generate.TokensDeposited(dps.FlowSymbol)
+ if err != nil {
+ return nil, fmt.Errorf("could not generate deposit event type: %w", err)
+ }
+ withdrawal, err := r.generate.TokensWithdrawn(dps.FlowSymbol)
+ if err != nil {
+ return nil, fmt.Errorf("could not generate withdrawal event type: %w", err)
+ }
+
+ // Retrieve the deposit and withdrawal events for the block (yes, all of them).
+ events, err := r.index.Events(height, flow.EventType(deposit), flow.EventType(withdrawal))
+ if err != nil {
+ return nil, fmt.Errorf("could not get events: %w", err)
+ }
+
+ // Convert events to operations.
+ ops, err := r.operations(txID, events)
+ if err != nil {
+ return nil, fmt.Errorf("could not convert events to operations: %w", err)
+ }
+
+ transaction := object.Transaction{
+ ID: rosettaTxID(txID),
+ Operations: ops,
+ }
+
+ return &transaction, nil
+}
+
+// Sequence retrieves the sequence number of an account's public key.
+func (r *Retriever) Sequence(rosBlockID identifier.Block, rosAccountID identifier.Account, index int) (uint64, error) {
+
+ // Run validation on the Rosetta block identifier. This will infer any
+ // missing data and return the height and block ID.
+ height, _, err := r.validate.Block(rosBlockID)
+ if err != nil {
+ return 0, fmt.Errorf("could not validate block: %w", err)
+ }
+
+ // Run validation on the Rosetta account identifier. This will return the
+ // native Flow address.
+ address, err := r.validate.Account(rosAccountID)
+ if err != nil {
+ return 0, fmt.Errorf("could not validate account: %w", err)
+ }
+
+ // Retrieve the key at the height of the given block and for the given
+ // address at the given index.
+ key, err := r.invoke.Key(height, address, index)
+ if err != nil {
+ return 0, fmt.Errorf("could not retrieve account: %w", err)
+ }
+
+ return key.SeqNumber, nil
+}
+
+// operations allows us to extract the operations for a transaction ID by using the given list of
+// events. In general, we retrieve all events for the block in question, so those should be passed in order to avoid
+// querying events for each transaction in a block.
+func (r *Retriever) operations(txID flow.Identifier, events []flow.Event) ([]*object.Operation, error) {
+
+ // These are the currently supported event types. The order here has to be kept the same so that we can keep
+ // deterministic operation indices, which is a requirement of the Rosetta API specification.
+ deposit, err := r.generate.TokensDeposited(dps.FlowSymbol)
+ if err != nil {
+ return nil, fmt.Errorf("could not generate deposit event type: %w", err)
+ }
+ withdrawal, err := r.generate.TokensWithdrawn(dps.FlowSymbol)
+ if err != nil {
+ return nil, fmt.Errorf("could not generate withdrawal event type: %w", err)
+ }
+ priorities := map[string]uint{
+ deposit: 1,
+ withdrawal: 2,
+ }
+
+ // We then start by filtering out all events that don't have the right transaction
+ // ID or which are not a supported type. Afterwards, we sort them by priority,
+ // which will make sure that we keep a deterministic index order for operations.
+ filtered := make([]flow.Event, 0, len(events))
+ for _, event := range events {
+ if event.TransactionID != txID {
+ continue
+ }
+ _, ok := priorities[string(event.Type)]
+ if !ok {
+ continue
+ }
+ filtered = append(filtered, event)
+ }
+ sort.Slice(filtered, func(i int, j int) bool {
+ return priorities[string(events[i].Type)] < priorities[string(events[j].Type)]
+ })
+
+ // Now we can convert each event to an operation, as they are both filtered for
+ // only supported ones and properly ordered.
+ ops := make([]*object.Operation, 0, len(filtered))
+ for _, event := range filtered {
+ op, err := r.convert.EventToOperation(event)
+ if errors.Is(err, ErrNoAddress) {
+ // this will happen when an event is not related to an account
+ continue
+ }
+ if errors.Is(err, ErrNotSupported) {
+ // this should never happen, but it's good defensive programming
+ continue
+ }
+ if err != nil {
+ return nil, fmt.Errorf("could not convert event to operation (tx: %s, type: %s): %w", event.TransactionID, event.Type, err)
+ }
+ ops = append(ops, op)
+ }
+
+ // Finally, we can assign the indices.
+ for index, op := range ops {
+ op.ID.Index = uint(index)
+ }
+
+ return ops, nil
+}
diff --git a/rosetta/retriever/retriever_internal_test.go b/rosetta/retriever/retriever_internal_test.go
new file mode 100755
index 0000000..3c211d1
--- /dev/null
+++ b/rosetta/retriever/retriever_internal_test.go
@@ -0,0 +1,106 @@
+// Copyright 2021 Optakt Labs OÜ
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+package retriever
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+
+ "github.com/optakt/flow-dps/models/dps"
+ "github.com/optakt/flow-rosetta/testing/mocks"
+)
+
+func TestNew(t *testing.T) {
+ params := mocks.GenericParams
+ index := mocks.BaselineReader(t)
+ validate := mocks.BaselineValidator(t)
+ generator := mocks.BaselineGenerator(t)
+ invoke := mocks.BaselineInvoker(t)
+ convert := mocks.BaselineConverter(t)
+
+ r := New(params, index, validate, generator, invoke, convert)
+
+ require.NotNil(t, r)
+ assert.Equal(t, params, r.params)
+ assert.Equal(t, index, r.index)
+ assert.Equal(t, validate, r.validate)
+ assert.Equal(t, generator, r.generate)
+ assert.Equal(t, invoke, r.invoke)
+ assert.Equal(t, convert, r.convert)
+}
+
+func BaselineRetriever(t *testing.T, opts ...func(*Retriever)) *Retriever {
+ t.Helper()
+
+ r := Retriever{
+ cfg: Config{TransactionLimit: 999},
+ params: mocks.GenericParams,
+ index: mocks.BaselineReader(t),
+ validate: mocks.BaselineValidator(t),
+ generate: mocks.BaselineGenerator(t),
+ invoke: mocks.BaselineInvoker(t),
+ convert: mocks.BaselineConverter(t),
+ }
+
+ for _, opt := range opts {
+ opt(&r)
+ }
+
+ return &r
+}
+
+func WithIndex(index dps.Reader) func(*Retriever) {
+ return func(retriever *Retriever) {
+ retriever.index = index
+ }
+}
+
+func WithValidator(validate Validator) func(*Retriever) {
+ return func(retriever *Retriever) {
+ retriever.validate = validate
+ }
+}
+
+func WithGenerator(generate Generator) func(*Retriever) {
+ return func(retriever *Retriever) {
+ retriever.generate = generate
+ }
+}
+
+func WithInvoker(invoke Invoker) func(*Retriever) {
+ return func(retriever *Retriever) {
+ retriever.invoke = invoke
+ }
+}
+
+func WithConverter(convert Converter) func(*Retriever) {
+ return func(retriever *Retriever) {
+ retriever.convert = convert
+ }
+}
+
+func WithParams(params dps.Params) func(*Retriever) {
+ return func(retriever *Retriever) {
+ retriever.params = params
+ }
+}
+
+func WithLimit(limit uint) func(*Retriever) {
+ return func(retriever *Retriever) {
+ retriever.cfg.TransactionLimit = limit
+ }
+}
diff --git a/rosetta/retriever/retriever_test.go b/rosetta/retriever/retriever_test.go
new file mode 100755
index 0000000..79e2a0e
--- /dev/null
+++ b/rosetta/retriever/retriever_test.go
@@ -0,0 +1,993 @@
+// Copyright 2021 Optakt Labs OÜ
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+package retriever_test
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+
+ "github.com/onflow/cadence"
+ "github.com/onflow/flow-go/model/flow"
+
+ "github.com/optakt/flow-dps/models/dps"
+ "github.com/optakt/flow-rosetta/rosetta/identifier"
+ "github.com/optakt/flow-rosetta/rosetta/object"
+ "github.com/optakt/flow-rosetta/rosetta/retriever"
+ "github.com/optakt/flow-rosetta/testing/mocks"
+)
+
+func TestRetriever_Oldest(t *testing.T) {
+ header := mocks.GenericHeader
+
+ t.Run("nominal case", func(t *testing.T) {
+ t.Parallel()
+
+ index := mocks.BaselineReader(t)
+ index.HeaderFunc = func(height uint64) (*flow.Header, error) {
+ assert.Equal(t, mocks.GenericHeight, height)
+ return mocks.GenericHeader, nil
+ }
+
+ ret := retriever.BaselineRetriever(t, retriever.WithIndex(index))
+
+ blockID, blockTime, err := ret.Oldest()
+
+ require.NoError(t, err)
+ wantRosBlockID := identifier.Block{
+ Index: &header.Height,
+ Hash: header.ID().String(),
+ }
+ assert.Equal(t, wantRosBlockID, blockID)
+ assert.Equal(t, header.Timestamp, blockTime)
+ })
+
+ t.Run("handles index.First failure", func(t *testing.T) {
+ t.Parallel()
+
+ index := mocks.BaselineReader(t)
+ index.FirstFunc = func() (uint64, error) {
+ return 0, mocks.GenericError
+ }
+
+ ret := retriever.BaselineRetriever(t, retriever.WithIndex(index))
+
+ _, _, err := ret.Oldest()
+
+ assert.Error(t, err)
+ })
+
+ t.Run("handles index.Header failure", func(t *testing.T) {
+ t.Parallel()
+
+ index := mocks.BaselineReader(t)
+ index.HeaderFunc = func(height uint64) (*flow.Header, error) {
+ return nil, mocks.GenericError
+ }
+
+ ret := retriever.BaselineRetriever(t, retriever.WithIndex(index))
+
+ _, _, err := ret.Oldest()
+
+ assert.Error(t, err)
+ })
+}
+
+func TestRetriever_Current(t *testing.T) {
+ header := mocks.GenericHeader
+
+ t.Run("nominal case", func(t *testing.T) {
+ t.Parallel()
+
+ index := mocks.BaselineReader(t)
+ index.HeaderFunc = func(height uint64) (*flow.Header, error) {
+ assert.Equal(t, header.Height, height)
+
+ return header, nil
+ }
+
+ ret := retriever.BaselineRetriever(t, retriever.WithIndex(index))
+
+ blockID, blockTime, err := ret.Current()
+
+ require.NoError(t, err)
+ assert.Equal(t, header.ID().String(), blockID.Hash)
+ assert.Equal(t, header.Timestamp, blockTime)
+ })
+
+ t.Run("handles index.Last failure", func(t *testing.T) {
+ t.Parallel()
+
+ index := mocks.BaselineReader(t)
+ index.LastFunc = func() (uint64, error) {
+ return 0, mocks.GenericError
+ }
+
+ ret := retriever.BaselineRetriever(t, retriever.WithIndex(index))
+
+ _, _, err := ret.Current()
+
+ assert.Error(t, err)
+ })
+
+ t.Run("handles index.Header failure", func(t *testing.T) {
+ t.Parallel()
+
+ index := mocks.BaselineReader(t)
+ index.HeaderFunc = func(uint64) (*flow.Header, error) {
+ return nil, mocks.GenericError
+ }
+
+ ret := retriever.BaselineRetriever(t, retriever.WithIndex(index))
+
+ _, _, err := ret.Current()
+
+ assert.Error(t, err)
+ })
+}
+
+func TestRetriever_Balances(t *testing.T) {
+ header := mocks.GenericHeader
+ account := mocks.GenericAccount
+ address := cadence.NewAddress(account.Address)
+ currency := mocks.GenericCurrency
+ rosBlockID := mocks.GenericRosBlockID
+ accountID := mocks.GenericAccountID(0)
+ op := mocks.GenericOperation(0)
+
+ t.Run("nominal case", func(t *testing.T) {
+ t.Parallel()
+
+ validator := mocks.BaselineValidator(t)
+ validator.AccountFunc = func(address identifier.Account) (flow.Address, error) {
+ assert.Equal(t, accountID, address)
+
+ return account.Address, nil
+ }
+ validator.BlockFunc = func(rosBlockID identifier.Block) (uint64, flow.Identifier, error) {
+ assert.Equal(t, rosBlockID, rosBlockID)
+
+ return header.Height, header.ID(), nil
+ }
+ validator.CurrencyFunc = func(currency identifier.Currency) (string, uint, error) {
+ assert.Equal(t, mocks.GenericCurrency, currency)
+
+ return currency.Symbol, currency.Decimals, nil
+ }
+
+ generator := mocks.BaselineGenerator(t)
+ generator.GetBalanceFunc = func(symbol string) ([]byte, error) {
+ assert.Equal(t, currency.Symbol, symbol)
+
+ return []byte(`test`), nil
+ }
+
+ invoker := mocks.BaselineInvoker(t)
+ invoker.ScriptFunc = func(height uint64, script []byte, parameters []cadence.Value) (cadence.Value, error) {
+ assert.Equal(t, rosBlockID.Index, &height)
+ assert.Equal(t, []byte(`test`), script)
+ require.Len(t, parameters, 1)
+ assert.Equal(t, address, parameters[0])
+
+ return mocks.GenericAmount(0), nil
+ }
+
+ ret := retriever.BaselineRetriever(
+ t,
+ retriever.WithGenerator(generator),
+ retriever.WithInvoker(invoker),
+ retriever.WithValidator(validator),
+ retriever.WithLimit(5),
+ )
+
+ blockID, amounts, err := ret.Balances(
+ rosBlockID,
+ accountID,
+ []identifier.Currency{currency},
+ )
+
+ require.NoError(t, err)
+ assert.Equal(t, rosBlockID, blockID)
+
+ wantAmounts := []object.Amount{
+ op.Amount,
+ }
+ assert.Equal(t, wantAmounts, amounts)
+ })
+
+ t.Run("handles invalid block", func(t *testing.T) {
+ t.Parallel()
+
+ validator := mocks.BaselineValidator(t)
+ validator.BlockFunc = func(identifier.Block) (uint64, flow.Identifier, error) {
+ return 0, flow.ZeroID, mocks.GenericError
+ }
+
+ ret := retriever.BaselineRetriever(t, retriever.WithValidator(validator))
+
+ _, _, err := ret.Balances(
+ rosBlockID,
+ accountID,
+ []identifier.Currency{currency},
+ )
+ assert.Error(t, err)
+ })
+
+ t.Run("handles invalid account", func(t *testing.T) {
+ t.Parallel()
+
+ validator := mocks.BaselineValidator(t)
+ validator.AccountFunc = func(identifier.Account) (flow.Address, error) {
+ return flow.EmptyAddress, mocks.GenericError
+ }
+
+ ret := retriever.BaselineRetriever(t, retriever.WithValidator(validator))
+
+ _, _, err := ret.Balances(
+ rosBlockID,
+ accountID,
+ []identifier.Currency{currency},
+ )
+ assert.Error(t, err)
+ })
+
+ t.Run("handles invalid currency", func(t *testing.T) {
+ t.Parallel()
+
+ validator := mocks.BaselineValidator(t)
+ validator.CurrencyFunc = func(identifier.Currency) (string, uint, error) {
+ return "", 0, mocks.GenericError
+ }
+
+ ret := retriever.BaselineRetriever(t, retriever.WithValidator(validator))
+
+ _, _, err := ret.Balances(
+ rosBlockID,
+ accountID,
+ []identifier.Currency{currency},
+ )
+ assert.Error(t, err)
+ })
+
+ t.Run("handles generate failure", func(t *testing.T) {
+ t.Parallel()
+
+ generator := mocks.BaselineGenerator(t)
+ generator.GetBalanceFunc = func(string) ([]byte, error) {
+ return nil, mocks.GenericError
+ }
+
+ ret := retriever.BaselineRetriever(t, retriever.WithGenerator(generator))
+
+ _, _, err := ret.Balances(
+ rosBlockID,
+ accountID,
+ []identifier.Currency{currency},
+ )
+ assert.Error(t, err)
+ })
+
+ t.Run("handles invoker failure", func(t *testing.T) {
+ t.Parallel()
+
+ invoker := mocks.BaselineInvoker(t)
+ invoker.ScriptFunc = func(uint64, []byte, []cadence.Value) (cadence.Value, error) {
+ return nil, mocks.GenericError
+ }
+
+ ret := retriever.BaselineRetriever(t, retriever.WithInvoker(invoker))
+
+ _, _, err := ret.Balances(
+ rosBlockID,
+ accountID,
+ []identifier.Currency{currency},
+ )
+ assert.Error(t, err)
+ })
+}
+
+func TestRetriever_Block(t *testing.T) {
+ header := mocks.GenericHeader
+ rosBlockID := mocks.GenericRosBlockID
+
+ transactions := mocks.GenericTransactionIDs(5)
+ withdrawalType := mocks.GenericEventType(0)
+ depositType := mocks.GenericEventType(1)
+ withdrawals := mocks.GenericEvents(2, withdrawalType)
+ deposits := mocks.GenericEvents(2, depositType)
+ events := append(withdrawals, deposits...)
+
+ t.Run("nominal case with limit not reached", func(t *testing.T) {
+ t.Parallel()
+
+ index := mocks.BaselineReader(t)
+ index.HeaderFunc = func(height uint64) (*flow.Header, error) {
+ assert.Equal(t, header.Height, height)
+
+ return header, nil
+ }
+ index.TransactionsByHeightFunc = func(height uint64) ([]flow.Identifier, error) {
+ assert.Equal(t, header.Height, height)
+
+ return transactions, nil
+ }
+ index.EventsFunc = func(height uint64, types ...flow.EventType) ([]flow.Event, error) {
+ assert.Equal(t, header.Height, height)
+
+ return events, nil
+ }
+
+ validator := mocks.BaselineValidator(t)
+ validator.BlockFunc = func(rosBlockID identifier.Block) (uint64, flow.Identifier, error) {
+ assert.Equal(t, rosBlockID, rosBlockID)
+
+ return header.Height, header.ID(), nil
+ }
+
+ generator := mocks.BaselineGenerator(t)
+ generator.TokensDepositedFunc = func(symbol string) (string, error) {
+ assert.Equal(t, symbol, dps.FlowSymbol)
+
+ return string(withdrawalType), nil
+ }
+ generator.TokensWithdrawnFunc = func(symbol string) (string, error) {
+ assert.Equal(t, symbol, dps.FlowSymbol)
+
+ return string(depositType), nil
+ }
+
+ convert := mocks.BaselineConverter(t)
+ convert.EventToOperationFunc = func(event flow.Event) (*object.Operation, error) {
+ var op object.Operation
+ switch event.Type {
+ case withdrawalType:
+ assert.Contains(t, withdrawals, event)
+ op = mocks.GenericOperation(0)
+ case depositType:
+ assert.Contains(t, deposits, event)
+ op = mocks.GenericOperation(1)
+ }
+
+ return &op, nil
+ }
+
+ ret := retriever.BaselineRetriever(
+ t,
+ retriever.WithGenerator(generator),
+ retriever.WithIndex(index),
+ retriever.WithValidator(validator),
+ retriever.WithConverter(convert),
+ retriever.WithLimit(6),
+ )
+
+ block, extra, err := ret.Block(rosBlockID)
+
+ require.NoError(t, err)
+ assert.Equal(t, rosBlockID, block.ID)
+ assert.Len(t, block.Transactions, 5)
+
+ assert.Empty(t, extra)
+ })
+
+ t.Run("nominal case with limit reached exactly", func(t *testing.T) {
+ t.Parallel()
+
+ index := mocks.BaselineReader(t)
+ index.HeaderFunc = func(height uint64) (*flow.Header, error) {
+ assert.Equal(t, header.Height, height)
+
+ return mocks.GenericHeader, nil
+ }
+ index.TransactionsByHeightFunc = func(height uint64) ([]flow.Identifier, error) {
+ assert.Equal(t, header.Height, height)
+
+ return mocks.GenericTransactionIDs(5), nil
+ }
+ index.EventsFunc = func(height uint64, types ...flow.EventType) ([]flow.Event, error) {
+ assert.Equal(t, header.Height, height)
+
+ return events, nil
+ }
+
+ validator := mocks.BaselineValidator(t)
+ validator.BlockFunc = func(rosBlockID identifier.Block) (uint64, flow.Identifier, error) {
+ assert.Equal(t, rosBlockID, rosBlockID)
+
+ return header.Height, header.ID(), nil
+ }
+
+ generator := mocks.BaselineGenerator(t)
+ generator.TokensDepositedFunc = func(symbol string) (string, error) {
+ assert.Equal(t, symbol, dps.FlowSymbol)
+
+ return string(depositType), nil
+ }
+ generator.TokensWithdrawnFunc = func(symbol string) (string, error) {
+ assert.Equal(t, symbol, dps.FlowSymbol)
+
+ return string(withdrawalType), nil
+ }
+
+ convert := mocks.BaselineConverter(t)
+ convert.EventToOperationFunc = func(event flow.Event) (*object.Operation, error) {
+
+ var op object.Operation
+ switch event.Type {
+ case withdrawalType:
+ assert.Contains(t, withdrawals, event)
+ op = mocks.GenericOperation(0)
+ case depositType:
+ assert.Contains(t, deposits, event)
+ op = mocks.GenericOperation(1)
+ }
+
+ return &op, nil
+ }
+
+ ret := retriever.BaselineRetriever(
+ t,
+ retriever.WithGenerator(generator),
+ retriever.WithIndex(index),
+ retriever.WithValidator(validator),
+ retriever.WithConverter(convert),
+ retriever.WithLimit(5),
+ )
+
+ block, extra, err := ret.Block(rosBlockID)
+
+ require.NoError(t, err)
+ assert.Len(t, block.Transactions, 5)
+ assert.Equal(t, rosBlockID, block.ID)
+
+ assert.Empty(t, extra)
+ })
+
+ t.Run("nominal case with more transactions than limit", func(t *testing.T) {
+ t.Parallel()
+
+ index := mocks.BaselineReader(t)
+ index.HeaderFunc = func(height uint64) (*flow.Header, error) {
+ assert.Equal(t, header.Height, height)
+
+ return mocks.GenericHeader, nil
+ }
+ index.TransactionsByHeightFunc = func(height uint64) ([]flow.Identifier, error) {
+ assert.Equal(t, header.Height, height)
+
+ return mocks.GenericTransactionIDs(6), nil
+ }
+ index.EventsFunc = func(height uint64, types ...flow.EventType) ([]flow.Event, error) {
+ assert.Equal(t, mocks.GenericHeight, height)
+
+ return events, nil
+ }
+ index.EventsFunc = func(height uint64, types ...flow.EventType) ([]flow.Event, error) {
+ assert.Equal(t, header.Height, height)
+
+ return events, nil
+ }
+
+ validator := mocks.BaselineValidator(t)
+ validator.BlockFunc = func(rosBlockID identifier.Block) (uint64, flow.Identifier, error) {
+ assert.Equal(t, rosBlockID, rosBlockID)
+
+ return header.Height, header.ID(), nil
+ }
+
+ generator := mocks.BaselineGenerator(t)
+ generator.TokensDepositedFunc = func(symbol string) (string, error) {
+ assert.Equal(t, symbol, dps.FlowSymbol)
+
+ return string(depositType), nil
+ }
+ generator.TokensWithdrawnFunc = func(symbol string) (string, error) {
+ assert.Equal(t, symbol, dps.FlowSymbol)
+
+ return string(withdrawalType), nil
+ }
+
+ convert := mocks.BaselineConverter(t)
+ convert.EventToOperationFunc = func(event flow.Event) (*object.Operation, error) {
+
+ var op object.Operation
+ switch event.Type {
+ case withdrawalType:
+ assert.Contains(t, withdrawals, event)
+ op = mocks.GenericOperation(0)
+ case depositType:
+ assert.Contains(t, deposits, event)
+ op = mocks.GenericOperation(1)
+ }
+
+ return &op, nil
+ }
+
+ ret := retriever.BaselineRetriever(
+ t,
+ retriever.WithGenerator(generator),
+ retriever.WithIndex(index),
+ retriever.WithValidator(validator),
+ retriever.WithConverter(convert),
+ retriever.WithLimit(5),
+ )
+
+ block, extra, err := ret.Block(rosBlockID)
+
+ require.NoError(t, err)
+ assert.Equal(t, rosBlockID, block.ID)
+ assert.Len(t, block.Transactions, 5)
+
+ assert.Len(t, extra, 1)
+ })
+
+ t.Run("handles block without transactions", func(t *testing.T) {
+ t.Parallel()
+
+ index := mocks.BaselineReader(t)
+ index.TransactionsByHeightFunc = func(uint64) ([]flow.Identifier, error) {
+ return []flow.Identifier{}, nil
+ }
+
+ ret := retriever.BaselineRetriever(t, retriever.WithIndex(index))
+
+ got, _, err := ret.Block(rosBlockID)
+ require.NoError(t, err)
+ assert.Empty(t, got.Transactions)
+ })
+
+ t.Run("handles block without relevant events", func(t *testing.T) {
+ t.Parallel()
+
+ index := mocks.BaselineReader(t)
+ index.EventsFunc = func(uint64, ...flow.EventType) ([]flow.Event, error) {
+ return []flow.Event{}, nil
+ }
+
+ ret := retriever.BaselineRetriever(t, retriever.WithIndex(index))
+
+ got, _, err := ret.Block(rosBlockID)
+ require.NoError(t, err)
+ require.NotEmpty(t, got.Transactions)
+ for _, tx := range got.Transactions {
+ assert.Empty(t, tx.Operations)
+ }
+ })
+
+ t.Run("handles invalid block", func(t *testing.T) {
+ t.Parallel()
+
+ validator := mocks.BaselineValidator(t)
+ validator.BlockFunc = func(identifier.Block) (uint64, flow.Identifier, error) {
+ return 0, flow.ZeroID, mocks.GenericError
+ }
+
+ ret := retriever.BaselineRetriever(t, retriever.WithValidator(validator))
+
+ _, _, err := ret.Block(rosBlockID)
+
+ assert.Error(t, err)
+ })
+
+ t.Run("handles deposit script generate failure", func(t *testing.T) {
+ t.Parallel()
+
+ generator := mocks.BaselineGenerator(t)
+ generator.TokensDepositedFunc = func(string) (string, error) {
+ return "", mocks.GenericError
+ }
+
+ ret := retriever.BaselineRetriever(t, retriever.WithGenerator(generator))
+
+ _, _, err := ret.Block(rosBlockID)
+
+ assert.Error(t, err)
+ })
+
+ t.Run("handles withdrawal script generate failure", func(t *testing.T) {
+ t.Parallel()
+
+ generator := mocks.BaselineGenerator(t)
+ generator.TokensWithdrawnFunc = func(string) (string, error) {
+ return "", mocks.GenericError
+ }
+
+ ret := retriever.BaselineRetriever(t, retriever.WithGenerator(generator))
+
+ _, _, err := ret.Block(rosBlockID)
+
+ assert.Error(t, err)
+ })
+
+ t.Run("handles index header retrieval failure", func(t *testing.T) {
+ t.Parallel()
+
+ index := mocks.BaselineReader(t)
+ index.HeaderFunc = func(uint64) (*flow.Header, error) {
+ return nil, mocks.GenericError
+ }
+
+ ret := retriever.BaselineRetriever(t, retriever.WithIndex(index))
+
+ _, _, err := ret.Block(rosBlockID)
+
+ assert.Error(t, err)
+ })
+
+ t.Run("handles index event retrieval failure", func(t *testing.T) {
+ t.Parallel()
+
+ index := mocks.BaselineReader(t)
+ index.EventsFunc = func(uint64, ...flow.EventType) ([]flow.Event, error) {
+ return nil, mocks.GenericError
+ }
+
+ ret := retriever.BaselineRetriever(t, retriever.WithIndex(index))
+
+ _, _, err := ret.Block(rosBlockID)
+ assert.Error(t, err)
+ })
+
+ t.Run("handles event converter failure", func(t *testing.T) {
+ t.Parallel()
+ convert := mocks.BaselineConverter(t)
+ convert.EventToOperationFunc = func(flow.Event) (*object.Operation, error) {
+ return nil, mocks.GenericError
+ }
+
+ ret := retriever.BaselineRetriever(t, retriever.WithConverter(convert))
+
+ _, _, err := ret.Block(rosBlockID)
+ assert.Error(t, err)
+ })
+}
+
+func TestRetriever_Transaction(t *testing.T) {
+ header := mocks.GenericHeader
+ rosBlockID := mocks.GenericRosBlockID
+
+ withdrawalType := mocks.GenericEventType(0)
+ depositType := mocks.GenericEventType(1)
+ withdrawals := mocks.GenericEvents(2, withdrawalType)
+ deposits := mocks.GenericEvents(2, depositType)
+ events := append(withdrawals, deposits...)
+
+ txQual := mocks.GenericTransactionQualifier(0)
+ txIDs := mocks.GenericTransactionIDs(5)
+
+ t.Run("nominal case", func(t *testing.T) {
+ t.Parallel()
+
+ validator := mocks.BaselineValidator(t)
+ validator.BlockFunc = func(rosBlockID identifier.Block) (uint64, flow.Identifier, error) {
+ assert.Equal(t, rosBlockID, rosBlockID)
+
+ return header.Height, header.ID(), nil
+ }
+ validator.TransactionFunc = func(transaction identifier.Transaction) (flow.Identifier, error) {
+ assert.Equal(t, txQual, transaction)
+
+ return txIDs[0], nil
+ }
+
+ generator := mocks.BaselineGenerator(t)
+ generator.TokensDepositedFunc = func(symbol string) (string, error) {
+ assert.Equal(t, dps.FlowSymbol, symbol)
+
+ return string(withdrawalType), nil
+ }
+ generator.TokensWithdrawnFunc = func(symbol string) (string, error) {
+ assert.Equal(t, dps.FlowSymbol, symbol)
+
+ return string(depositType), nil
+ }
+
+ index := mocks.BaselineReader(t)
+ index.EventsFunc = func(height uint64, types ...flow.EventType) ([]flow.Event, error) {
+ assert.Equal(t, header.Height, height)
+ require.Len(t, types, 2)
+ assert.Equal(t, withdrawalType, types[0])
+ assert.Equal(t, depositType, types[1])
+
+ return events, nil
+ }
+ index.TransactionsByHeightFunc = func(height uint64) ([]flow.Identifier, error) {
+ assert.Equal(t, header.Height, height)
+
+ return txIDs, nil
+ }
+
+ convert := mocks.BaselineConverter(t)
+ convert.EventToOperationFunc = func(event flow.Event) (*object.Operation, error) {
+
+ var op object.Operation
+ switch event.Type {
+ case withdrawalType:
+ assert.Contains(t, withdrawals, event)
+ op = mocks.GenericOperation(0)
+ case depositType:
+ assert.Contains(t, deposits, event)
+ op = mocks.GenericOperation(1)
+ }
+
+ return &op, nil
+ }
+
+ ret := retriever.BaselineRetriever(
+ t,
+ retriever.WithGenerator(generator),
+ retriever.WithIndex(index),
+ retriever.WithValidator(validator),
+ retriever.WithConverter(convert),
+ )
+
+ got, err := ret.Transaction(rosBlockID, txQual)
+
+ require.NoError(t, err)
+ assert.Equal(t, txQual, got.ID)
+ assert.Len(t, got.Operations, 2)
+ })
+
+ t.Run("handles transaction with no relevant operations", func(t *testing.T) {
+ t.Parallel()
+
+ index := mocks.BaselineReader(t)
+ index.EventsFunc = func(uint64, ...flow.EventType) ([]flow.Event, error) {
+ return []flow.Event{
+ {
+ Type: mocks.GenericEventType(0),
+ // Here we use the wrong resource ID on purpose so that it does not match any of transaction ID.
+ TransactionID: mocks.GenericSeal(0).ID(),
+ },
+ }, nil
+ }
+
+ ret := retriever.BaselineRetriever(t, retriever.WithIndex(index))
+
+ got, err := ret.Transaction(rosBlockID, txQual)
+
+ require.NoError(t, err)
+ assert.Empty(t, got.Operations)
+ })
+
+ t.Run("handles invalid block", func(t *testing.T) {
+ t.Parallel()
+
+ validator := mocks.BaselineValidator(t)
+ validator.BlockFunc = func(identifier.Block) (uint64, flow.Identifier, error) {
+ return 0, flow.ZeroID, mocks.GenericError
+ }
+
+ ret := retriever.BaselineRetriever(t, retriever.WithValidator(validator))
+
+ _, err := ret.Transaction(rosBlockID, mocks.GenericTransactionQualifier(0))
+
+ assert.Error(t, err)
+ })
+
+ t.Run("handles invalid transaction", func(t *testing.T) {
+ t.Parallel()
+
+ validator := mocks.BaselineValidator(t)
+ validator.TransactionFunc = func(identifier.Transaction) (flow.Identifier, error) {
+ return flow.ZeroID, mocks.GenericError
+ }
+
+ ret := retriever.BaselineRetriever(t, retriever.WithValidator(validator))
+
+ _, err := ret.Transaction(rosBlockID, mocks.GenericTransactionQualifier(0))
+
+ assert.Error(t, err)
+ })
+
+ t.Run("block does not contain transaction", func(t *testing.T) {
+ index := mocks.BaselineReader(t)
+ index.TransactionsByHeightFunc = func(uint64) ([]flow.Identifier, error) {
+ return []flow.Identifier{}, nil
+ }
+
+ ret := retriever.BaselineRetriever(t, retriever.WithIndex(index))
+
+ _, err := ret.Transaction(rosBlockID, mocks.GenericTransactionQualifier(0))
+
+ assert.Error(t, err)
+ })
+
+ t.Run("handles transactions index failure", func(t *testing.T) {
+ index := mocks.BaselineReader(t)
+ index.TransactionsByHeightFunc = func(uint64) ([]flow.Identifier, error) {
+ return nil, mocks.GenericError
+ }
+
+ ret := retriever.BaselineRetriever(t, retriever.WithIndex(index))
+
+ _, err := ret.Transaction(rosBlockID, mocks.GenericTransactionQualifier(0))
+
+ assert.Error(t, err)
+ })
+
+ t.Run("handles deposit script generate failure", func(t *testing.T) {
+ t.Parallel()
+
+ generator := mocks.BaselineGenerator(t)
+ generator.TokensDepositedFunc = func(string) (string, error) {
+ return "", mocks.GenericError
+ }
+
+ ret := retriever.BaselineRetriever(t, retriever.WithGenerator(generator))
+
+ _, err := ret.Transaction(rosBlockID, mocks.GenericTransactionQualifier(0))
+
+ assert.Error(t, err)
+ })
+
+ t.Run("handles withdrawal script generate failure", func(t *testing.T) {
+ t.Parallel()
+
+ generator := mocks.BaselineGenerator(t)
+ generator.TokensWithdrawnFunc = func(string) (string, error) {
+ return "", mocks.GenericError
+ }
+
+ ret := retriever.BaselineRetriever(t, retriever.WithGenerator(generator))
+
+ _, err := ret.Transaction(rosBlockID, mocks.GenericTransactionQualifier(0))
+
+ assert.Error(t, err)
+ })
+
+ t.Run("handles index event retrieval failure", func(t *testing.T) {
+ t.Parallel()
+
+ index := mocks.BaselineReader(t)
+ index.EventsFunc = func(uint64, ...flow.EventType) ([]flow.Event, error) {
+ return nil, mocks.GenericError
+ }
+
+ ret := retriever.BaselineRetriever(t, retriever.WithIndex(index))
+
+ _, err := ret.Transaction(rosBlockID, mocks.GenericTransactionQualifier(0))
+
+ assert.Error(t, err)
+ })
+
+ t.Run("handles converter failure", func(t *testing.T) {
+ t.Parallel()
+
+ convert := mocks.BaselineConverter(t)
+ convert.EventToOperationFunc = func(flow.Event) (*object.Operation, error) {
+ return nil, mocks.GenericError
+ }
+
+ ret := retriever.BaselineRetriever(t, retriever.WithConverter(convert))
+
+ _, err := ret.Transaction(mocks.GenericRosBlockID, mocks.GenericTransactionQualifier(0))
+
+ assert.Error(t, err)
+ })
+}
+
+func TestRetriever_Sequence(t *testing.T) {
+ rosBlockID := mocks.GenericRosBlockID
+ accountID := mocks.GenericAccountID(0)
+ address := mocks.GenericAddress(0)
+ key := mocks.GenericAccount.Keys[0]
+ header := mocks.GenericHeader
+
+ t.Run("nominal case", func(t *testing.T) {
+ t.Parallel()
+
+ validator := mocks.BaselineValidator(t)
+ validator.BlockFunc = func(blockID identifier.Block) (uint64, flow.Identifier, error) {
+ assert.Equal(t, rosBlockID, blockID)
+
+ return header.Height, header.ID(), nil
+ }
+ validator.AccountFunc = func(gotAccountID identifier.Account) (flow.Address, error) {
+ assert.Equal(t, accountID, gotAccountID)
+
+ return address, nil
+ }
+
+ invoker := mocks.BaselineInvoker(t)
+ invoker.KeyFunc = func(height uint64, gotAddress flow.Address, index int) (*flow.AccountPublicKey, error) {
+ assert.Equal(t, header.Height, height)
+ assert.Equal(t, address, gotAddress)
+ assert.Equal(t, 0, index)
+
+ return &key, nil
+ }
+
+ ret := retriever.New(
+ mocks.GenericParams,
+ mocks.BaselineReader(t),
+ validator,
+ mocks.BaselineGenerator(t),
+ invoker,
+ mocks.BaselineConverter(t),
+ )
+
+ seqNum, err := ret.Sequence(rosBlockID, accountID, 0)
+
+ require.NoError(t, err)
+ assert.Equal(t, key.SeqNumber, seqNum)
+ })
+
+ t.Run("handles validator failure on block", func(t *testing.T) {
+ t.Parallel()
+
+ validator := mocks.BaselineValidator(t)
+ validator.BlockFunc = func(identifier.Block) (uint64, flow.Identifier, error) {
+ return 0, flow.ZeroID, mocks.GenericError
+ }
+
+ ret := retriever.New(
+ mocks.GenericParams,
+ mocks.BaselineReader(t),
+ validator,
+ mocks.BaselineGenerator(t),
+ mocks.BaselineInvoker(t),
+ mocks.BaselineConverter(t),
+ )
+
+ _, err := ret.Sequence(rosBlockID, accountID, 0)
+
+ assert.Error(t, err)
+ })
+
+ t.Run("handles validator failure on account", func(t *testing.T) {
+ t.Parallel()
+
+ validator := mocks.BaselineValidator(t)
+ validator.AccountFunc = func(identifier.Account) (flow.Address, error) {
+ return flow.EmptyAddress, mocks.GenericError
+ }
+
+ ret := retriever.New(
+ mocks.GenericParams,
+ mocks.BaselineReader(t),
+ validator,
+ mocks.BaselineGenerator(t),
+ mocks.BaselineInvoker(t),
+ mocks.BaselineConverter(t),
+ )
+
+ _, err := ret.Sequence(rosBlockID, accountID, 0)
+
+ assert.Error(t, err)
+ })
+
+ t.Run("handles invoker failure on key", func(t *testing.T) {
+ t.Parallel()
+
+ invoker := mocks.BaselineInvoker(t)
+ invoker.KeyFunc = func(uint64, flow.Address, int) (*flow.AccountPublicKey, error) {
+ return nil, mocks.GenericError
+ }
+
+ ret := retriever.New(
+ mocks.GenericParams,
+ mocks.BaselineReader(t),
+ mocks.BaselineValidator(t),
+ mocks.BaselineGenerator(t),
+ invoker,
+ mocks.BaselineConverter(t),
+ )
+
+ _, err := ret.Sequence(rosBlockID, accountID, 0)
+
+ assert.Error(t, err)
+ })
+}
diff --git a/rosetta/retriever/validator.go b/rosetta/retriever/validator.go
new file mode 100755
index 0000000..e85fc0f
--- /dev/null
+++ b/rosetta/retriever/validator.go
@@ -0,0 +1,29 @@
+// Copyright 2021 Optakt Labs OÜ
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+package retriever
+
+import (
+ "github.com/onflow/flow-go/model/flow"
+
+ "github.com/optakt/flow-rosetta/rosetta/identifier"
+)
+
+// Validator represents something that can validate account, block and transaction identifiers as well as currencies.
+type Validator interface {
+ Account(rosAccountID identifier.Account) (address flow.Address, err error)
+ Block(rosBlockID identifier.Block) (height uint64, blockID flow.Identifier, err error)
+ Transaction(rosTxID identifier.Transaction) (txID flow.Identifier, err error)
+ Currency(rosCurrency identifier.Currency) (symbol string, decimals uint, err error)
+}
diff --git a/rosetta/scripts/generator.go b/rosetta/scripts/generator.go
new file mode 100755
index 0000000..e462e2b
--- /dev/null
+++ b/rosetta/scripts/generator.go
@@ -0,0 +1,100 @@
+// Copyright 2021 Optakt Labs OÜ
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+package scripts
+
+import (
+ "bytes"
+ "fmt"
+ "text/template"
+
+ "github.com/optakt/flow-dps/models/dps"
+)
+
+// Generator dynamically generates Cadence scripts from templates.
+type Generator struct {
+ params dps.Params
+ getBalance *template.Template
+ transferTokens *template.Template
+ tokensDeposited *template.Template
+ tokensWithdrawn *template.Template
+}
+
+// NewGenerator returns a Generator using the given parameters.
+func NewGenerator(params dps.Params) *Generator {
+ g := Generator{
+ params: params,
+ getBalance: template.Must(template.New("get_balance").Parse(getBalance)),
+ transferTokens: template.Must(template.New("transfer_tokens").Parse(transferTokens)),
+ tokensDeposited: template.Must(template.New("tokensDeposited").Parse(tokensDeposited)),
+ tokensWithdrawn: template.Must(template.New("withdrawal").Parse(tokensWithdrawn)),
+ }
+ return &g
+}
+
+// GetBalance generates a Cadence script to retrieve the balance of an account.
+func (g *Generator) GetBalance(symbol string) ([]byte, error) {
+ return g.bytes(g.getBalance, symbol)
+}
+
+// TransferTokens generates a Cadence script to operate a token transfer transaction.
+func (g *Generator) TransferTokens(symbol string) ([]byte, error) {
+ return g.bytes(g.transferTokens, symbol)
+}
+
+// TokensDeposited generates a Cadence script that matches the Flow event for tokens being deposited.
+func (g *Generator) TokensDeposited(symbol string) (string, error) {
+ return g.string(g.tokensDeposited, symbol)
+}
+
+// TokensWithdrawn generates a Cadence script that matches the Flow event for tokens being withdrawn.
+func (g *Generator) TokensWithdrawn(symbol string) (string, error) {
+ return g.string(g.tokensWithdrawn, symbol)
+}
+
+func (g *Generator) string(template *template.Template, symbol string) (string, error) {
+ buf, err := g.compile(template, symbol)
+ if err != nil {
+ return "", fmt.Errorf("could not compile template: %w", err)
+ }
+ return buf.String(), nil
+}
+
+func (g *Generator) bytes(template *template.Template, symbol string) ([]byte, error) {
+ buf, err := g.compile(template, symbol)
+ if err != nil {
+ return nil, fmt.Errorf("could not compile template: %w", err)
+ }
+ return buf.Bytes(), nil
+}
+
+func (g *Generator) compile(template *template.Template, symbol string) (*bytes.Buffer, error) {
+ token, ok := g.params.Tokens[symbol]
+ if !ok {
+ return nil, fmt.Errorf("invalid token symbol (%s)", symbol)
+ }
+ data := struct {
+ Params dps.Params
+ Token dps.Token
+ }{
+ Params: g.params,
+ Token: token,
+ }
+ buf := &bytes.Buffer{}
+ err := template.Execute(buf, data)
+ if err != nil {
+ return nil, fmt.Errorf("could not execute template: %w", err)
+ }
+ return buf, nil
+}
diff --git a/rosetta/scripts/get_balance.go b/rosetta/scripts/get_balance.go
new file mode 100755
index 0000000..72f784d
--- /dev/null
+++ b/rosetta/scripts/get_balance.go
@@ -0,0 +1,34 @@
+// Copyright 2021 Optakt Labs OÜ
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+package scripts
+
+// Adopted from:
+// https://github.com/onflow/flow-core-contracts/blob/master/transactions/flowToken/scripts/get_balance.cdc
+
+const getBalance = `// This script reads the balance field of an account's FlowToken Balance
+
+import FungibleToken from 0x{{.Params.FungibleToken}}
+import {{.Token.Type}} from 0x{{.Token.Address}}
+
+pub fun main(account: Address): UFix64 {
+
+ let vaultRef = getAccount(account)
+ .getCapability({{.Token.Balance}})
+ .borrow<&{{.Token.Type}}.Vault{FungibleToken.Balance}>()
+ ?? panic("Could not borrow Balance reference to the Vault")
+
+ return vaultRef.balance
+}
+`
diff --git a/rosetta/scripts/tokens_deposited.go b/rosetta/scripts/tokens_deposited.go
new file mode 100755
index 0000000..bd41706
--- /dev/null
+++ b/rosetta/scripts/tokens_deposited.go
@@ -0,0 +1,17 @@
+// Copyright 2021 Optakt Labs OÜ
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+package scripts
+
+const tokensDeposited = "A.{{.Token.Address}}.{{.Token.Type}}.TokensDeposited"
diff --git a/rosetta/scripts/tokens_withdrawn.go b/rosetta/scripts/tokens_withdrawn.go
new file mode 100755
index 0000000..1a8da79
--- /dev/null
+++ b/rosetta/scripts/tokens_withdrawn.go
@@ -0,0 +1,17 @@
+// Copyright 2021 Optakt Labs OÜ
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+package scripts
+
+const tokensWithdrawn = "A.{{.Token.Address}}.{{.Token.Type}}.TokensWithdrawn"
diff --git a/rosetta/scripts/transfer_tokens.go b/rosetta/scripts/transfer_tokens.go
new file mode 100755
index 0000000..4bbae6a
--- /dev/null
+++ b/rosetta/scripts/transfer_tokens.go
@@ -0,0 +1,57 @@
+// Copyright 2021 Optakt Labs OÜ
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+package scripts
+
+// Adopted from:
+// https://github.com/onflow/flow-core-contracts/blob/master/transactions/flowToken/transfer_tokens.cdc
+
+const transferTokens = `// This transaction is a template for a transaction that
+// could be used by anyone to send tokens to another account
+// that has been set up to receive tokens.
+//
+// The withdraw amount and the account from getAccount
+// would be the parameters to the transaction
+
+import FungibleToken from 0x{{.Params.FungibleToken}}
+import {{.Token.Type}} from 0x{{.Token.Address}}
+
+transaction(amount: UFix64, to: Address) {
+
+ // The Vault resource that holds the tokens that are being transferred
+ let sentVault: @FungibleToken.Vault
+
+ prepare(signer: AuthAccount) {
+
+ // Get a reference to the signer's stored vault
+ let vaultRef = signer.borrow<&{{.Token.Type}}.Vault>(from: {{.Token.Vault}})
+ ?? panic("Could not borrow reference to the owner's Vault!")
+
+ // Withdraw tokens from the signer's stored vault
+ self.sentVault <- vaultRef.withdraw(amount: amount)
+ }
+
+ execute {
+
+ // Get a reference to the recipient's Receiver
+ let receiverRef = getAccount(to)
+ .getCapability({{.Token.Receiver}})
+ .borrow<&{FungibleToken.Receiver}>()
+ ?? panic("Could not borrow receiver reference to the recipient's Vault")
+
+ // Deposit the withdrawn tokens in the recipient's receiver
+ receiverRef.deposit(from: <-self.sentVault)
+ }
+}
+`
diff --git a/rosetta/submitter/api.go b/rosetta/submitter/api.go
new file mode 100755
index 0000000..305005d
--- /dev/null
+++ b/rosetta/submitter/api.go
@@ -0,0 +1,28 @@
+// Copyright 2021 Optakt Labs OÜ
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+package submitter
+
+import (
+ "context"
+
+ "google.golang.org/grpc"
+
+ sdk "github.com/onflow/flow-go-sdk"
+)
+
+// API represents something that can be used to submit transactions.
+type API interface {
+ SendTransaction(ctx context.Context, tx sdk.Transaction, opts ...grpc.CallOption) error
+}
diff --git a/rosetta/submitter/submitter.go b/rosetta/submitter/submitter.go
new file mode 100755
index 0000000..99593eb
--- /dev/null
+++ b/rosetta/submitter/submitter.go
@@ -0,0 +1,46 @@
+// Copyright 2021 Optakt Labs OÜ
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+package submitter
+
+import (
+ "context"
+ "fmt"
+
+ sdk "github.com/onflow/flow-go-sdk"
+)
+
+// Submitter submits transactions for execution.
+type Submitter struct {
+ // api is typically a Flow SDK client.
+ api API
+}
+
+// New creates a new Submitter that uses the given API.
+func New(api API) *Submitter {
+ s := Submitter{
+ api: api,
+ }
+ return &s
+}
+
+// Transaction submits the given transaction for execution.
+func (s *Submitter) Transaction(tx *sdk.Transaction) error {
+ err := s.api.SendTransaction(context.Background(), *tx)
+ if err != nil {
+ return fmt.Errorf("could not submit transaction: %w", err)
+ }
+
+ return nil
+}
diff --git a/rosetta/transactor/convert.go b/rosetta/transactor/convert.go
new file mode 100755
index 0000000..cfefa57
--- /dev/null
+++ b/rosetta/transactor/convert.go
@@ -0,0 +1,35 @@
+// Copyright 2021 Optakt Labs OÜ
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+package transactor
+
+import (
+ sdk "github.com/onflow/flow-go-sdk"
+ "github.com/onflow/flow-go/model/flow"
+
+ "github.com/optakt/flow-rosetta/rosetta/identifier"
+)
+
+func rosettaTxID(txID sdk.Identifier) identifier.Transaction {
+ return identifier.Transaction{
+ Hash: txID.String(),
+ }
+}
+
+func rosettaBlockID(height uint64, blockID flow.Identifier) identifier.Block {
+ return identifier.Block{
+ Index: &height,
+ Hash: blockID.String(),
+ }
+}
diff --git a/rosetta/transactor/errors.go b/rosetta/transactor/errors.go
new file mode 100755
index 0000000..4c53bbf
--- /dev/null
+++ b/rosetta/transactor/errors.go
@@ -0,0 +1,48 @@
+// Copyright 2021 Optakt Labs OÜ
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+package transactor
+
+// Error descriptions for common errors.
+const (
+ // Transaction actor errors.
+ authorizersInvalid = "invalid number of authorizers"
+ payerInvalid = "invalid transaction payer"
+ proposerInvalid = "invalid transaction proposer"
+
+ // Transaction signature errors.
+ signerInvalid = "invalid signer account"
+ sigInvalid = "provided signature is not valid"
+ sigEncoding = "invalid signature payload"
+ payloadSigFound = "unexpected payload signature found"
+ envelopeSigFound = "unexpected envelope signature found"
+ envelopeSigCountInvalid = "unexpected number of envelope signatures"
+ sigCountInvalid = "invalid number of signatures"
+ sigAlgoInvalid = "invalid signature algorithm"
+
+ // Transaction script errors.
+ scriptInvalid = "transaction text is not valid token transfer script"
+ scriptArgsInvalid = "invalid number of arguments"
+ amountUnparseable = "could not parse transaction amount"
+ amountInvalid = "invalid amount"
+ receiverUnparseable = "could not parse transaction receiver address"
+
+ // Operations/intent errors.
+ opsInvalid = "invalid number of operations"
+ opsAmountsMismatch = "transfer amounts do not match"
+ currenciesInvalid = "invalid currencies found"
+ opAmountUnparseable = "could not parse amount"
+ opTypeInvalid = "only transfer operations are supported"
+ keyInvalid = "invalid account key"
+)
diff --git a/rosetta/transactor/generator.go b/rosetta/transactor/generator.go
new file mode 100755
index 0000000..1492e03
--- /dev/null
+++ b/rosetta/transactor/generator.go
@@ -0,0 +1,21 @@
+// Copyright 2021 Optakt Labs OÜ
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+package transactor
+
+// Generator represents something that can generate Cadence scripts for transferring tokens
+// between two accounts.
+type Generator interface {
+ TransferTokens(symbol string) ([]byte, error)
+}
diff --git a/rosetta/transactor/intent.go b/rosetta/transactor/intent.go
new file mode 100755
index 0000000..aad788c
--- /dev/null
+++ b/rosetta/transactor/intent.go
@@ -0,0 +1,29 @@
+// Copyright 2021 Optakt Labs OÜ
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+package transactor
+
+import (
+ "github.com/onflow/cadence"
+ "github.com/onflow/flow-go/model/flow"
+)
+
+// Intent describes the intent of a set of two Rosetta operations.
+type Intent struct {
+ From flow.Address
+ To flow.Address
+ Amount cadence.UFix64
+ Payer flow.Address
+ Proposer flow.Address
+}
diff --git a/rosetta/transactor/invoker.go b/rosetta/transactor/invoker.go
new file mode 100755
index 0000000..b0c3550
--- /dev/null
+++ b/rosetta/transactor/invoker.go
@@ -0,0 +1,24 @@
+// Copyright 2021 Optakt Labs OÜ
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+package transactor
+
+import (
+ "github.com/onflow/flow-go/model/flow"
+)
+
+// Invoker represents something that can retrieve account public keys at any given height.
+type Invoker interface {
+ Key(height uint64, address flow.Address, index int) (*flow.AccountPublicKey, error)
+}
diff --git a/rosetta/transactor/parser.go b/rosetta/transactor/parser.go
new file mode 100755
index 0000000..2b6b745
--- /dev/null
+++ b/rosetta/transactor/parser.go
@@ -0,0 +1,292 @@
+// Copyright 2021 Optakt Labs OÜ
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+package transactor
+
+import (
+ "bytes"
+ "encoding/hex"
+ "fmt"
+ "strconv"
+
+ cjson "github.com/onflow/cadence/encoding/json"
+ sdk "github.com/onflow/flow-go-sdk"
+ "github.com/onflow/flow-go-sdk/crypto"
+ "github.com/onflow/flow-go/model/flow"
+
+ "github.com/optakt/flow-dps/models/dps"
+ "github.com/optakt/flow-rosetta/rosetta/failure"
+ "github.com/optakt/flow-rosetta/rosetta/identifier"
+ "github.com/optakt/flow-rosetta/rosetta/object"
+)
+
+// TransactionParser is a wrapper around a pointer to a sdk.Transaction which exposes methods to
+// individually parse different elements of the transaction.
+type TransactionParser struct {
+ tx *sdk.Transaction
+ validate Validator
+ generate Generator
+ invoke Invoker
+}
+
+// BlockID parses the transaction's BlockID.
+func (p *TransactionParser) BlockID() (identifier.Block, error) {
+ // Validate the reference block identifier.
+ refBlockID := identifier.Block{
+ Hash: p.tx.ReferenceBlockID.String(),
+ }
+
+ height, blockID, err := p.validate.Block(refBlockID)
+ if err != nil {
+ return identifier.Block{}, fmt.Errorf("invalid reference block: %w", err)
+ }
+
+ return rosettaBlockID(height, blockID), nil
+}
+
+// Sequence parses the transaction's sequence number.
+func (p *TransactionParser) Sequence() uint64 {
+ return p.tx.ProposalKey.SequenceNumber
+}
+
+// Signers parses the transaction's signer accounts.
+func (p *TransactionParser) Signers() ([]identifier.Account, error) {
+ // Since we only support sender as the payer/proposer, we never expect any payload signatures.
+ if len(p.tx.PayloadSignatures) > 0 {
+ return nil, failure.InvalidSignature{
+ Description: failure.NewDescription(payloadSigFound,
+ failure.WithInt("signatures", len(p.tx.PayloadSignatures))),
+ }
+ }
+
+ // We may be parsing an unsigned transaction - if that's the case, we're done.
+ if len(p.tx.EnvelopeSignatures) == 0 {
+ return nil, nil
+ }
+
+ // We don't support multiple signatures.
+ if len(p.tx.EnvelopeSignatures) > 1 {
+ return nil, failure.InvalidSignature{
+ Description: failure.NewDescription(envelopeSigCountInvalid,
+ failure.WithInt("signatures", len(p.tx.EnvelopeSignatures))),
+ }
+ }
+
+ // Validate that it is the sender who signed the transaction.
+ signer := p.tx.EnvelopeSignatures[0].Address
+ authorizer := p.tx.Authorizers[0]
+ if signer != authorizer {
+ return nil, failure.InvalidSignature{
+ Description: failure.NewDescription(signerInvalid,
+ failure.WithString("have_signer", signer.String()),
+ failure.WithString("want_signer", authorizer.String()),
+ failure.WithString("signature", hex.EncodeToString(p.tx.EnvelopeSignatures[0].Signature))),
+ }
+ }
+
+ // Check that the signature is valid.
+ address := flow.BytesToAddress(signer[:])
+
+ rosBlockID := identifier.Block{Hash: p.tx.ReferenceBlockID.Hex()}
+ height, _, err := p.validate.Block(rosBlockID)
+ if err != nil {
+ return nil, fmt.Errorf("could not validate block: %w", err)
+ }
+
+ key, err := p.invoke.Key(height, address, 0)
+ if err != nil {
+ return nil, fmt.Errorf("could not retrieve key: %w", err)
+ }
+
+ // NOTE: signature verification is ported from the DefaultSignatureVerifier
+ // => https://github.com/onflow/flow-go/blob/master/fvm/crypto/crypto.go
+ hasher, err := crypto.NewHasher(key.HashAlgo)
+ if err != nil {
+ return nil, fmt.Errorf("could not get new hasher: %w", err)
+ }
+
+ message := p.tx.EnvelopeMessage()
+ message = append(sdk.TransactionDomainTag[:], message...)
+
+ signature := p.tx.EnvelopeSignatures[0].Signature
+
+ valid, err := key.PublicKey.Verify(signature, message, hasher)
+ if err != nil {
+ return nil, fmt.Errorf("could not verify transaction signature: %w", err)
+ }
+ if !valid {
+ return nil, failure.InvalidSignature{
+ Description: failure.NewDescription(sigInvalid,
+ failure.WithString("signature", hex.EncodeToString(signature))),
+ }
+ }
+
+ sender := identifier.Account{
+ Address: authorizer.String(),
+ }
+
+ // Validate the sender address.
+ _, err = p.validate.Account(sender)
+ if err != nil {
+ return nil, fmt.Errorf("invalid sender account: %w", err)
+ }
+
+ // Create the signers list.
+ signers := []identifier.Account{
+ sender,
+ }
+
+ return signers, nil
+}
+
+// Operations parses the transaction's operations.
+func (p *TransactionParser) Operations() ([]object.Operation, error) {
+ // Validate the transaction actors. We expect a single authorizer - the sender account.
+ // For now, the sender must also be the proposer and the payer for the transaction.
+ if len(p.tx.Authorizers) != requiredAuthorizers {
+ return nil, failure.InvalidAuthorizers{
+ Have: uint(len(p.tx.Authorizers)),
+ Want: requiredAuthorizers,
+ Description: failure.NewDescription(authorizersInvalid),
+ }
+ }
+
+ authorizer := p.tx.Authorizers[0]
+ sender := identifier.Account{
+ Address: authorizer.String(),
+ }
+
+ // Validate the sender address.
+ _, err := p.validate.Account(sender)
+ if err != nil {
+ return nil, fmt.Errorf("invalid sender account: %w", err)
+ }
+
+ // Verify that the sender is the payer and the proposer.
+ if p.tx.Payer != authorizer {
+ return nil, failure.InvalidPayer{
+ Have: flow.BytesToAddress(p.tx.Payer[:]),
+ Want: flow.BytesToAddress(authorizer[:]),
+ Description: failure.NewDescription(payerInvalid),
+ }
+ }
+ if p.tx.ProposalKey.Address != authorizer {
+ return nil, failure.InvalidProposer{
+ Have: flow.BytesToAddress(p.tx.ProposalKey.Address[:]),
+ Want: flow.BytesToAddress(authorizer[:]),
+ Description: failure.NewDescription(proposerInvalid),
+ }
+ }
+
+ // Verify the transaction script is the token transfer script.
+ script, err := p.generate.TransferTokens(dps.FlowSymbol)
+ if err != nil {
+ return nil, fmt.Errorf("could not generate transfer script: %w", err)
+ }
+ if !bytes.Equal(script, p.tx.Script) {
+ return nil, failure.InvalidScript{
+ Script: string(p.tx.Script),
+ Description: failure.NewDescription(scriptInvalid),
+ }
+ }
+
+ // Verify that the transaction script has the correct number of arguments.
+ args := p.tx.Arguments
+ if len(args) != requiredArguments {
+ return nil, failure.InvalidArguments{
+ Have: uint(len(args)),
+ Want: requiredArguments,
+ Description: failure.NewDescription(scriptArgsInvalid),
+ }
+ }
+
+ // Parse and validate the amount argument.
+ val, err := cjson.Decode(args[0])
+ if err != nil {
+ return nil, failure.InvalidAmount{
+ Amount: string(args[0]),
+ Description: failure.NewDescription(amountUnparseable,
+ failure.WithErr(err)),
+ }
+ }
+ amountArg, ok := val.ToGoValue().(uint64)
+ if !ok {
+ return nil, failure.InvalidAmount{
+ Amount: string(args[0]),
+ Description: failure.NewDescription(amountInvalid),
+ }
+ }
+ amount := strconv.FormatUint(amountArg, 10)
+
+ // Parse and validate receiver script argument.
+ val, err = cjson.Decode(args[1])
+ if err != nil {
+ return nil, failure.InvalidReceiver{
+ Receiver: string(args[1]),
+ Description: failure.NewDescription(receiverUnparseable,
+ failure.WithErr(err)),
+ }
+ }
+ addr := flow.HexToAddress(val.String())
+ receiver := identifier.Account{
+ Address: addr.String(),
+ }
+ _, err = p.validate.Account(receiver)
+ if err != nil {
+ return nil, fmt.Errorf("invalid receiver account: %w", err)
+ }
+
+ // Create the send operation.
+ sendOp := object.Operation{
+ ID: identifier.Operation{
+ Index: 0,
+ NetworkIndex: nil, // optional, omitted for now
+ },
+ AccountID: sender,
+ Type: dps.OperationTransfer,
+ Amount: object.Amount{
+ Value: "-" + amount,
+ Currency: identifier.Currency{
+ Symbol: dps.FlowSymbol,
+ Decimals: dps.FlowDecimals,
+ },
+ },
+ Status: "", // must NOT be set for non-submitted transactions
+ }
+
+ // Create the receive operation.
+ receiveOp := object.Operation{
+ ID: identifier.Operation{
+ Index: 1,
+ NetworkIndex: nil, // optional, omitted for now
+ },
+ AccountID: receiver,
+ Type: dps.OperationTransfer,
+ Amount: object.Amount{
+ Value: amount,
+ Currency: identifier.Currency{
+ Symbol: dps.FlowSymbol,
+ Decimals: dps.FlowDecimals,
+ },
+ },
+ Status: "", // must NOT be set for non-submitted transactions
+ }
+
+ ops := []object.Operation{
+ sendOp,
+ receiveOp,
+ }
+
+ return ops, nil
+}
diff --git a/rosetta/transactor/parser_internal_test.go b/rosetta/transactor/parser_internal_test.go
new file mode 100755
index 0000000..3caf7ee
--- /dev/null
+++ b/rosetta/transactor/parser_internal_test.go
@@ -0,0 +1,62 @@
+// Copyright 2021 Optakt Labs OÜ
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+package transactor
+
+import (
+ "testing"
+
+ sdk "github.com/onflow/flow-go-sdk"
+
+ "github.com/optakt/flow-rosetta/testing/mocks"
+)
+
+func BaselineTransactionParser(t *testing.T, opts ...func(parser *TransactionParser)) *TransactionParser {
+ p := TransactionParser{
+ tx: sdk.NewTransaction(),
+ validate: mocks.BaselineValidator(t),
+ generate: mocks.BaselineGenerator(t),
+ invoke: mocks.BaselineInvoker(t),
+ }
+
+ for _, opt := range opts {
+ opt(&p)
+ }
+
+ return &p
+}
+
+func InjectTransaction(tx *sdk.Transaction) func(*TransactionParser) {
+ return func(parser *TransactionParser) {
+ parser.tx = tx
+ }
+}
+
+func InjectValidator(validate Validator) func(*TransactionParser) {
+ return func(parser *TransactionParser) {
+ parser.validate = validate
+ }
+}
+
+func InjectGenerator(generate Generator) func(*TransactionParser) {
+ return func(parser *TransactionParser) {
+ parser.generate = generate
+ }
+}
+
+func InjectInvoker(invoke Invoker) func(*TransactionParser) {
+ return func(parser *TransactionParser) {
+ parser.invoke = invoke
+ }
+}
diff --git a/rosetta/transactor/parser_test.go b/rosetta/transactor/parser_test.go
new file mode 100755
index 0000000..7f702ff
--- /dev/null
+++ b/rosetta/transactor/parser_test.go
@@ -0,0 +1,613 @@
+// Copyright 2021 Optakt Labs OÜ
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+package transactor_test
+
+import (
+ "crypto/rand"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+
+ "github.com/onflow/cadence"
+ cjson "github.com/onflow/cadence/encoding/json"
+ sdk "github.com/onflow/flow-go-sdk"
+ sdkcrypto "github.com/onflow/flow-go-sdk/crypto"
+ "github.com/onflow/flow-go/crypto"
+ chash "github.com/onflow/flow-go/crypto/hash"
+ "github.com/onflow/flow-go/model/flow"
+
+ "github.com/optakt/flow-rosetta/rosetta/failure"
+ "github.com/optakt/flow-rosetta/rosetta/identifier"
+ "github.com/optakt/flow-rosetta/rosetta/transactor"
+ "github.com/optakt/flow-rosetta/testing/mocks"
+)
+
+func TestTransactionParser_BlockID(t *testing.T) {
+ header := mocks.GenericHeader
+ blockID := header.ID()
+ index := uint64(84)
+
+ t.Run("nominal case", func(t *testing.T) {
+ t.Parallel()
+
+ tx := &sdk.Transaction{
+ ReferenceBlockID: sdk.HashToID(blockID[:]),
+ }
+
+ validator := mocks.BaselineValidator(t)
+ validator.BlockFunc = func(rosBlockID identifier.Block) (uint64, flow.Identifier, error) {
+ assert.Equal(t, tx.ReferenceBlockID.String(), rosBlockID.Hash)
+
+ return index, blockID, nil
+ }
+
+ p := transactor.BaselineTransactionParser(
+ t,
+ transactor.InjectTransaction(tx),
+ transactor.InjectValidator(validator),
+ )
+
+ got, err := p.BlockID()
+
+ require.NoError(t, err)
+ assert.Equal(t, tx.ReferenceBlockID.String(), got.Hash)
+ assert.Equal(t, index, *got.Index)
+ })
+
+ t.Run("handles invalid block data", func(t *testing.T) {
+ t.Parallel()
+
+ tx := &sdk.Transaction{
+ ReferenceBlockID: sdk.HashToID(blockID[:]),
+ }
+
+ validator := mocks.BaselineValidator(t)
+ validator.BlockFunc = func(identifier.Block) (uint64, flow.Identifier, error) {
+ return 0, flow.ZeroID, mocks.GenericError
+ }
+
+ p := transactor.BaselineTransactionParser(
+ t,
+ transactor.InjectTransaction(tx),
+ transactor.InjectValidator(validator),
+ )
+
+ _, err := p.BlockID()
+
+ assert.Error(t, err)
+ })
+}
+
+func TestTransactionParser_Sequence(t *testing.T) {
+ tx := &sdk.Transaction{
+ ProposalKey: sdk.ProposalKey{SequenceNumber: 42},
+ }
+
+ p := transactor.BaselineTransactionParser(t, transactor.InjectTransaction(tx))
+
+ got := p.Sequence()
+
+ assert.Equal(t, tx.ProposalKey.SequenceNumber, got)
+}
+
+func TestTransactionParser_Signers(t *testing.T) {
+ header := mocks.GenericHeader
+ blockID := header.ID()
+
+ key, err := generateKey()
+ require.NoError(t, err)
+
+ // We need to specify a weight of 1000 because otherwise multiple public keys would be required, as
+ // the total required weight for an account's signature to be considered valid has to be equal to 1000.
+ // For some reason this 1000 magic number is not exposed anywhere that I could find in Flow.
+ pubKey := key.PublicKey(1000)
+
+ senderID := mocks.GenericAccountID(0)
+ senderAddr := mocks.GenericAddress(0)
+ sender := sdk.HexToAddress(senderAddr.Hex())
+ receiver := sdk.HexToAddress(mocks.GenericAddress(1).Hex())
+ signature := sdk.TransactionSignature{
+ Address: sender,
+ SignerIndex: 64,
+ KeyIndex: 128,
+ }
+
+ invoker := mocks.BaselineInvoker(t)
+ invoker.KeyFunc = func(height uint64, address flow.Address, index int) (*flow.AccountPublicKey, error) {
+ return &pubKey, nil
+ }
+
+ tx := &sdk.Transaction{
+ ReferenceBlockID: sdk.HashToID(blockID[:]),
+ Authorizers: []sdk.Address{sender},
+ EnvelopeSignatures: []sdk.TransactionSignature{signature},
+ }
+
+ signer := sdkcrypto.NewInMemorySigner(key.PrivateKey, key.HashAlgo)
+ message := tx.EnvelopeMessage()
+ message = append(sdk.TransactionDomainTag[:], message...)
+
+ sig, err := signer.Sign(message)
+ require.NoError(t, err)
+
+ tx.EnvelopeSignatures[0].Signature = sig
+
+ t.Run("nominal case with unsigned transaction", func(t *testing.T) {
+ t.Parallel()
+
+ p := transactor.BaselineTransactionParser(t,
+ transactor.InjectTransaction(sdk.NewTransaction()),
+ transactor.InjectInvoker(invoker),
+ )
+
+ got, err := p.Signers()
+
+ require.NoError(t, err)
+ assert.Zero(t, got)
+ })
+
+ t.Run("nominal case with signed transaction", func(t *testing.T) {
+ t.Parallel()
+
+ invoker := mocks.BaselineInvoker(t)
+ invoker.KeyFunc = func(height uint64, address flow.Address, index int) (*flow.AccountPublicKey, error) {
+ assert.Equal(t, header.Height, height)
+ assert.Equal(t, senderAddr, address)
+ assert.Zero(t, index)
+
+ return &pubKey, nil
+ }
+
+ p := transactor.BaselineTransactionParser(t, transactor.InjectTransaction(tx), transactor.InjectInvoker(invoker))
+
+ got, err := p.Signers()
+
+ require.NoError(t, err)
+ assert.Len(t, got, 1)
+ assert.Equal(t, senderID, got[0])
+ })
+
+ t.Run("handles case where transaction contains a payload signature (which it should not)", func(t *testing.T) {
+ t.Parallel()
+
+ // Copy the valid transaction and only add a payload signature to it to trigger a failure.
+
+ tx := &sdk.Transaction{
+ ReferenceBlockID: sdk.HashToID(blockID[:]),
+ Authorizers: []sdk.Address{sender},
+ EnvelopeSignatures: []sdk.TransactionSignature{signature},
+ PayloadSignatures: []sdk.TransactionSignature{signature},
+ }
+
+ p := transactor.BaselineTransactionParser(t, transactor.InjectTransaction(tx), transactor.InjectInvoker(invoker))
+
+ _, err := p.Signers()
+
+ require.Error(t, err)
+ assert.ErrorAs(t, err, &failure.InvalidSignature{})
+ })
+
+ t.Run("handles case where there are multiple envelope signatures", func(t *testing.T) {
+ t.Parallel()
+
+ tx := &sdk.Transaction{
+ ReferenceBlockID: sdk.HashToID(blockID[:]),
+ Authorizers: []sdk.Address{sender},
+ EnvelopeSignatures: []sdk.TransactionSignature{signature, signature, signature},
+ }
+
+ p := transactor.BaselineTransactionParser(t, transactor.InjectTransaction(tx), transactor.InjectInvoker(invoker))
+
+ _, err := p.Signers()
+
+ require.Error(t, err)
+ assert.ErrorAs(t, err, &failure.InvalidSignature{})
+ })
+
+ t.Run("handles case where transaction signer is not the sender", func(t *testing.T) {
+ t.Parallel()
+
+ tx := &sdk.Transaction{
+ ReferenceBlockID: sdk.HashToID(blockID[:]),
+ Authorizers: []sdk.Address{receiver},
+ EnvelopeSignatures: []sdk.TransactionSignature{signature},
+ }
+
+ p := transactor.BaselineTransactionParser(t, transactor.InjectTransaction(tx), transactor.InjectInvoker(invoker))
+
+ _, err := p.Signers()
+
+ require.Error(t, err)
+ assert.ErrorAs(t, err, &failure.InvalidSignature{})
+ })
+
+ t.Run("handles case where transaction has invalid block ID", func(t *testing.T) {
+ t.Parallel()
+
+ validator := mocks.BaselineValidator(t)
+ validator.BlockFunc = func(identifier.Block) (uint64, flow.Identifier, error) {
+ return 0, flow.ZeroID, mocks.GenericError
+ }
+
+ p := transactor.BaselineTransactionParser(
+ t,
+ transactor.InjectTransaction(tx),
+ transactor.InjectValidator(validator),
+ transactor.InjectInvoker(invoker),
+ )
+
+ _, err := p.Signers()
+
+ assert.Error(t, err)
+ })
+
+ t.Run("handles invoker failure on Key", func(t *testing.T) {
+ t.Parallel()
+
+ invoker := mocks.BaselineInvoker(t)
+ invoker.KeyFunc = func(uint64, flow.Address, int) (*flow.AccountPublicKey, error) {
+ return nil, mocks.GenericError
+ }
+
+ p := transactor.BaselineTransactionParser(
+ t,
+ transactor.InjectTransaction(tx),
+ transactor.InjectInvoker(invoker),
+ )
+
+ _, err := p.Signers()
+
+ assert.Error(t, err)
+ })
+
+ t.Run("handles signature and key mismatch", func(t *testing.T) {
+ t.Parallel()
+
+ invoker := mocks.BaselineInvoker(t)
+ invoker.KeyFunc = func(uint64, flow.Address, int) (*flow.AccountPublicKey, error) {
+ // This is not the signature that was used to sign the data in the envelope, so
+ // the verification should fail.
+ mockSignature := mocks.GenericAccount.Keys[0]
+
+ return &mockSignature, nil
+ }
+
+ p := transactor.BaselineTransactionParser(t, transactor.InjectTransaction(tx), transactor.InjectInvoker(invoker))
+
+ _, err := p.Signers()
+
+ require.Error(t, err)
+ assert.ErrorAs(t, err, &failure.InvalidSignature{})
+ })
+
+ t.Run("handles invalid sender account", func(t *testing.T) {
+ t.Parallel()
+
+ validator := mocks.BaselineValidator(t)
+ validator.AccountFunc = func(identifier.Account) (flow.Address, error) {
+ return flow.EmptyAddress, mocks.GenericError
+ }
+
+ p := transactor.BaselineTransactionParser(
+ t,
+ transactor.InjectTransaction(tx),
+ transactor.InjectInvoker(invoker),
+ transactor.InjectValidator(validator),
+ )
+
+ _, err := p.Signers()
+
+ assert.Error(t, err)
+ })
+}
+
+func TestTransactionParser_Operations(t *testing.T) {
+ sender := sdk.HexToAddress(mocks.GenericAddress(0).Hex())
+ receiverAddr := mocks.GenericAddress(1)
+ receiver := sdk.HexToAddress(receiverAddr.Hex())
+
+ amount := mocks.GenericAmount(0)
+ amountData, err := cjson.Encode(amount)
+ require.NoError(t, err)
+
+ cadenceAddr := cadence.BytesToAddress(receiverAddr.Bytes())
+ addressData, err := cjson.Encode(cadenceAddr)
+ require.NoError(t, err)
+
+ tx := &sdk.Transaction{
+ Payer: sender,
+ ProposalKey: sdk.ProposalKey{Address: sender},
+ Authorizers: []sdk.Address{sender},
+ Script: mocks.GenericBytes,
+ Arguments: [][]byte{amountData, addressData},
+ }
+
+ t.Run("nominal case", func(t *testing.T) {
+ t.Parallel()
+
+ p := transactor.BaselineTransactionParser(t, transactor.InjectTransaction(tx))
+
+ got, err := p.Operations()
+
+ require.NoError(t, err)
+ assert.NotEmpty(t, got)
+ })
+
+ t.Run("handles invalid number of authorizers", func(t *testing.T) {
+ t.Parallel()
+
+ tx := &sdk.Transaction{
+ Authorizers: []sdk.Address{sender, receiver, sender},
+ }
+
+ p := transactor.BaselineTransactionParser(t, transactor.InjectTransaction(tx))
+
+ _, err := p.Operations()
+
+ require.Error(t, err)
+ assert.ErrorAs(t, err, &failure.InvalidAuthorizers{})
+ })
+
+ t.Run("handles invalid authorizer account", func(t *testing.T) {
+ t.Parallel()
+
+ tx := &sdk.Transaction{
+ Authorizers: []sdk.Address{sender},
+ }
+
+ validator := mocks.BaselineValidator(t)
+ validator.AccountFunc = func(identifier.Account) (flow.Address, error) {
+ return flow.EmptyAddress, mocks.GenericError
+ }
+
+ p := transactor.BaselineTransactionParser(
+ t,
+ transactor.InjectTransaction(tx),
+ transactor.InjectValidator(validator),
+ )
+
+ _, err := p.Operations()
+
+ assert.Error(t, err)
+ })
+
+ t.Run("handles mismatch between authorizer and payer", func(t *testing.T) {
+ t.Parallel()
+
+ tx := &sdk.Transaction{
+ Payer: receiver,
+ Authorizers: []sdk.Address{sender},
+ }
+
+ p := transactor.BaselineTransactionParser(t, transactor.InjectTransaction(tx))
+
+ _, err := p.Operations()
+
+ require.Error(t, err)
+ assert.ErrorAs(t, err, &failure.InvalidPayer{})
+ })
+
+ t.Run("handles mismatch between proposal key address and payer", func(t *testing.T) {
+ t.Parallel()
+
+ tx := &sdk.Transaction{
+ Payer: sender,
+ ProposalKey: sdk.ProposalKey{Address: receiver},
+ Authorizers: []sdk.Address{sender},
+ }
+
+ p := transactor.BaselineTransactionParser(t, transactor.InjectTransaction(tx))
+
+ _, err := p.Operations()
+
+ require.Error(t, err)
+ assert.ErrorAs(t, err, &failure.InvalidProposer{})
+ })
+
+ t.Run("handles transfer token generation failure", func(t *testing.T) {
+ t.Parallel()
+
+ generator := mocks.BaselineGenerator(t)
+ generator.TransferTokensFunc = func(string) ([]byte, error) {
+ return nil, mocks.GenericError
+ }
+
+ p := transactor.BaselineTransactionParser(
+ t,
+ transactor.InjectTransaction(tx),
+ transactor.InjectGenerator(generator),
+ )
+
+ _, err := p.Operations()
+
+ assert.Error(t, err)
+ })
+
+ t.Run("handles transfer token script mismatch", func(t *testing.T) {
+ t.Parallel()
+
+ generator := mocks.BaselineGenerator(t)
+ generator.TransferTokensFunc = func(string) ([]byte, error) {
+ return []byte{}, nil
+ }
+
+ p := transactor.BaselineTransactionParser(
+ t,
+ transactor.InjectTransaction(tx),
+ transactor.InjectGenerator(generator),
+ )
+
+ _, err := p.Operations()
+
+ require.Error(t, err)
+ assert.ErrorAs(t, err, &failure.InvalidScript{})
+ })
+
+ t.Run("handles invalid number of arguments (0)", func(t *testing.T) {
+ t.Parallel()
+
+ tx := &sdk.Transaction{
+ Payer: sender,
+ ProposalKey: sdk.ProposalKey{Address: sender},
+ Authorizers: []sdk.Address{sender},
+ Script: mocks.GenericBytes,
+ Arguments: [][]byte{}, // No argument.
+ }
+
+ p := transactor.BaselineTransactionParser(
+ t,
+ transactor.InjectTransaction(tx),
+ )
+
+ _, err := p.Operations()
+
+ require.Error(t, err)
+ assert.ErrorAs(t, err, &failure.InvalidArguments{})
+ })
+
+ t.Run("handles invalid number of arguments (<)", func(t *testing.T) {
+ t.Parallel()
+
+ tx := &sdk.Transaction{
+ Payer: sender,
+ ProposalKey: sdk.ProposalKey{Address: sender},
+ Authorizers: []sdk.Address{sender},
+ Script: mocks.GenericBytes,
+ Arguments: [][]byte{amountData}, // Only one argument.
+ }
+
+ p := transactor.BaselineTransactionParser(
+ t,
+ transactor.InjectTransaction(tx),
+ )
+
+ _, err := p.Operations()
+
+ require.Error(t, err)
+ assert.ErrorAs(t, err, &failure.InvalidArguments{})
+ })
+
+ t.Run("handles invalid number of arguments (>)", func(t *testing.T) {
+ t.Parallel()
+
+ tx := &sdk.Transaction{
+ Payer: sender,
+ ProposalKey: sdk.ProposalKey{Address: sender},
+ Authorizers: []sdk.Address{sender},
+ Script: mocks.GenericBytes,
+ Arguments: [][]byte{amountData, addressData, amountData}, // Three arguments.
+ }
+
+ p := transactor.BaselineTransactionParser(
+ t,
+ transactor.InjectTransaction(tx),
+ )
+
+ _, err := p.Operations()
+
+ require.Error(t, err)
+ assert.ErrorAs(t, err, &failure.InvalidArguments{})
+ })
+
+ t.Run("handles invalid amount argument (not a uint)", func(t *testing.T) {
+ t.Parallel()
+
+ tx := &sdk.Transaction{
+ Payer: sender,
+ ProposalKey: sdk.ProposalKey{Address: sender},
+ Authorizers: []sdk.Address{sender},
+ Script: mocks.GenericBytes,
+ Arguments: [][]byte{addressData, addressData}, // First argument is not an uint.
+ }
+
+ p := transactor.BaselineTransactionParser(
+ t,
+ transactor.InjectTransaction(tx),
+ )
+
+ _, err := p.Operations()
+
+ require.Error(t, err)
+ assert.ErrorAs(t, err, &failure.InvalidAmount{})
+ })
+
+ t.Run("handles invalid amount argument (not json-encoded)", func(t *testing.T) {
+ t.Parallel()
+
+ tx := &sdk.Transaction{
+ Payer: sender,
+ ProposalKey: sdk.ProposalKey{Address: sender},
+ Authorizers: []sdk.Address{sender},
+ Script: mocks.GenericBytes,
+ Arguments: [][]byte{mocks.GenericBytes, addressData}, // First argument is not json-encoded.
+ }
+
+ p := transactor.BaselineTransactionParser(
+ t,
+ transactor.InjectTransaction(tx),
+ )
+
+ _, err := p.Operations()
+
+ require.Error(t, err)
+ assert.ErrorAs(t, err, &failure.InvalidAmount{})
+ })
+
+ t.Run("handles invalid address argument (not json-encoded)", func(t *testing.T) {
+ t.Parallel()
+
+ tx := &sdk.Transaction{
+ Payer: sender,
+ ProposalKey: sdk.ProposalKey{Address: sender},
+ Authorizers: []sdk.Address{sender},
+ Script: mocks.GenericBytes,
+ Arguments: [][]byte{amountData, mocks.GenericBytes}, // Second argument is not json-encoded.
+ }
+
+ p := transactor.BaselineTransactionParser(
+ t,
+ transactor.InjectTransaction(tx),
+ )
+
+ _, err := p.Operations()
+
+ require.Error(t, err)
+ assert.ErrorAs(t, err, &failure.InvalidReceiver{})
+ })
+}
+
+func generateKey() (*flow.AccountPrivateKey, error) {
+ seed := make([]byte, crypto.KeyGenSeedMaxLenECDSA)
+
+ _, err := rand.Read(seed)
+ if err != nil {
+ return nil, err
+ }
+
+ signAlgo := crypto.ECDSAP256
+ hashAlgo := chash.SHA3_256
+
+ key, err := crypto.GeneratePrivateKey(signAlgo, seed)
+ if err != nil {
+ return nil, err
+ }
+
+ return &flow.AccountPrivateKey{
+ PrivateKey: key,
+ SignAlgo: key.Algorithm(),
+ HashAlgo: hashAlgo,
+ }, nil
+}
diff --git a/rosetta/transactor/submitter.go b/rosetta/transactor/submitter.go
new file mode 100755
index 0000000..793205f
--- /dev/null
+++ b/rosetta/transactor/submitter.go
@@ -0,0 +1,24 @@
+// Copyright 2021 Optakt Labs OÜ
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+package transactor
+
+import (
+ sdk "github.com/onflow/flow-go-sdk"
+)
+
+// Submitter represents something that can submit transactions.
+type Submitter interface {
+ Transaction(tx *sdk.Transaction) error
+}
diff --git a/rosetta/transactor/transactor.go b/rosetta/transactor/transactor.go
new file mode 100755
index 0000000..c75c9f0
--- /dev/null
+++ b/rosetta/transactor/transactor.go
@@ -0,0 +1,419 @@
+// Copyright 2021 Optakt Labs OÜ
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+package transactor
+
+import (
+ "encoding/base64"
+ "encoding/hex"
+ "encoding/json"
+ "fmt"
+ "sort"
+ "strconv"
+
+ "github.com/onflow/cadence"
+ sdk "github.com/onflow/flow-go-sdk"
+ "github.com/onflow/flow-go-sdk/crypto"
+ "github.com/onflow/flow-go/model/flow"
+
+ "github.com/optakt/flow-dps/models/dps"
+ "github.com/optakt/flow-rosetta/rosetta/failure"
+ "github.com/optakt/flow-rosetta/rosetta/identifier"
+ "github.com/optakt/flow-rosetta/rosetta/object"
+)
+
+const (
+ requiredAuthorizers = 1 // we only support one authorizer per transaction
+ requiredArguments = 2 // transactions need to arguments (amount & receiver)
+ requiredOperations = 2 // transactions are made of two operations (deposit & withdrawal)
+ requiredAlgorithm = "ecdsa" // transactions are signed with ECSDA
+)
+
+// Transactor can determine the transaction intent from an array of Rosetta
+// operations, create a Flow transaction from a transaction intent and
+// translate a Flow transaction back to an array of Rosetta operations.
+type Transactor struct {
+ validate Validator
+ generate Generator
+ invoke Invoker
+ submit Submitter
+}
+
+// Parser represents something that can parse a transaction into individual parts.
+type Parser interface {
+ BlockID() (identifier.Block, error)
+ Sequence() uint64
+ Signers() ([]identifier.Account, error)
+ Operations() ([]object.Operation, error)
+}
+
+// New creates a new transactor to handle interactions with Flow transactions.
+func New(validate Validator, generate Generator, invoke Invoker, submit Submitter) *Transactor {
+
+ p := Transactor{
+ validate: validate,
+ generate: generate,
+ invoke: invoke,
+ submit: submit,
+ }
+
+ return &p
+}
+
+// DeriveIntent derives a transaction Intent from two operations given as input.
+// Specified operations should be symmetrical, a deposit and a withdrawal from two
+// different accounts. At the moment, the only fields taken into account are the
+// account IDs, amounts and type of operation.
+func (t *Transactor) DeriveIntent(operations []object.Operation) (*Intent, error) {
+
+ // Verify that we have exactly two operations.
+ if len(operations) != requiredOperations {
+ return nil, failure.InvalidOperations{
+ Description: failure.NewDescription(opsInvalid),
+ Want: requiredOperations,
+ Have: uint(len(operations)),
+ }
+ }
+
+ // Parse amounts.
+ amounts := make([]int64, requiredOperations)
+ for i, op := range operations {
+ amount, err := strconv.ParseInt(op.Amount.Value, 10, 64)
+ if err != nil {
+ return nil, failure.InvalidIntent{
+ Description: failure.NewDescription(opAmountUnparseable,
+ failure.WithString("amount", op.Amount.Value),
+ failure.WithErr(err),
+ ),
+ }
+ }
+ amounts[i] = amount
+ }
+
+ // Verify that the amounts match.
+ if amounts[0] != -amounts[1] {
+ return nil, failure.InvalidIntent{
+ Description: failure.NewDescription(opsAmountsMismatch,
+ failure.WithString("first_amount", operations[0].Amount.Value),
+ failure.WithString("second_amount", operations[1].Amount.Value),
+ ),
+ }
+ }
+
+ // Sort the operations so that the send operation (negative amount) comes first.
+ sort.Slice(operations, func(i int, j int) bool {
+ return amounts[i] < amounts[j]
+ })
+ sort.Slice(amounts, func(i int, j int) bool {
+ return amounts[i] < amounts[j]
+ })
+
+ // Validate the currencies specified for deposit and withdrawal.
+ send := operations[0]
+ receive := operations[1]
+ sendSymbol, _, err := t.validate.Currency(send.Amount.Currency)
+ if err != nil {
+ return nil, fmt.Errorf("invalid sender currency: %w", err)
+ }
+ receiveSymbol, _, err := t.validate.Currency(receive.Amount.Currency)
+ if err != nil {
+ return nil, fmt.Errorf("invalid receiver currency: %w", err)
+ }
+
+ // Make sure that both operations are for FLOW tokens.
+ if sendSymbol != dps.FlowSymbol || receiveSymbol != dps.FlowSymbol {
+ return nil, failure.InvalidIntent{
+ Description: failure.NewDescription(currenciesInvalid,
+ failure.WithString("sender", send.AccountID.Address),
+ failure.WithString("receiver", receive.AccountID.Address),
+ failure.WithString("withdrawal_currency", send.Amount.Currency.Symbol),
+ failure.WithString("deposit_currency", receive.Amount.Currency.Symbol)),
+ }
+ }
+
+ // Validate the sender and the receiver account IDs.
+ _, err = t.validate.Account(send.AccountID)
+ if err != nil {
+ return nil, fmt.Errorf("invalid sender account: %w", err)
+ }
+ _, err = t.validate.Account(receive.AccountID)
+ if err != nil {
+ return nil, fmt.Errorf("invalid receiver account: %w", err)
+ }
+
+ // Validate that the specified operations are transfers.
+ if send.Type != dps.OperationTransfer || receive.Type != dps.OperationTransfer {
+ return nil, failure.InvalidIntent{
+ Description: failure.NewDescription(opTypeInvalid,
+ failure.WithString("withdrawal_type", send.Type),
+ failure.WithString("deposit_type", receive.Type),
+ ),
+ }
+ }
+
+ // The smaller amount is first, so the second one should always have the
+ // positive number.
+ amount := amounts[1]
+ intent := Intent{
+ From: flow.HexToAddress(send.AccountID.Address),
+ To: flow.HexToAddress(receive.AccountID.Address),
+ Amount: cadence.UFix64(amount),
+ Payer: flow.HexToAddress(send.AccountID.Address),
+ Proposer: flow.HexToAddress(send.AccountID.Address),
+ }
+
+ return &intent, nil
+}
+
+// CompileTransaction creates a complete Flow transaction from the given intent and metadata.
+func (t *Transactor) CompileTransaction(rosBlockID identifier.Block, intent *Intent, sequence uint64) (string, error) {
+
+ // Generate script for the token transfer.
+ script, err := t.generate.TransferTokens(dps.FlowSymbol)
+ if err != nil {
+ return "", fmt.Errorf("could not generate transfer script: %w", err)
+ }
+
+ // Create the transaction.
+ unsignedTx := sdk.NewTransaction().
+ SetScript(script).
+ SetReferenceBlockID(sdk.HexToID(rosBlockID.Hash)).
+ SetPayer(sdk.Address(intent.Payer)).
+ SetProposalKey(sdk.Address(intent.Proposer), 0, sequence).
+ AddAuthorizer(sdk.Address(intent.From)).
+ SetGasLimit(flow.DefaultMaxTransactionGasLimit)
+
+ receiver := cadence.NewAddress(flow.BytesToAddress(intent.To.Bytes()))
+
+ // Add the script arguments - the amount and the receiver.
+ // NOTE: This can only fail if the argument can not be encoded using the
+ // Cadence JSON encoder, which will never happen here.
+ _ = unsignedTx.AddArgument(intent.Amount)
+ _ = unsignedTx.AddArgument(receiver)
+
+ payload, err := t.encodeTransaction(unsignedTx)
+ if err != nil {
+ return "", fmt.Errorf("could not encode transaction: %w", err)
+ }
+
+ return payload, nil
+}
+
+// HashPayload returns the algorithm and hash of a given unsigned transaction when signed by
+// a given account's public key.
+func (t *Transactor) HashPayload(rosBlockID identifier.Block, unsigned string, signer identifier.Account) (string, string, error) {
+
+ unsignedTx, err := t.decodeTransaction(unsigned)
+ if err != nil {
+ return "", "", fmt.Errorf("could not decode transaction: %w", err)
+ }
+
+ // Validate block.
+ height, _, err := t.validate.Block(rosBlockID)
+ if err != nil {
+ return "", "", fmt.Errorf("could not validate block: %w", err)
+ }
+
+ // Validate address.
+ address, err := t.validate.Account(signer)
+ if err != nil {
+ return "", "", fmt.Errorf("could not validate account: %w", err)
+ }
+
+ key, err := t.invoke.Key(height, address, 0)
+ if err != nil {
+ return "", "", failure.InvalidKey{
+ Description: failure.NewDescription(keyInvalid, failure.WithErr(err)),
+ Height: height,
+ Address: address,
+ Index: 0,
+ }
+ }
+
+ message := unsignedTx.EnvelopeMessage()
+ message = append(flow.TransactionDomainTag[:], message...)
+
+ hasher, err := crypto.NewHasher(key.HashAlgo)
+ if err != nil {
+ return "", "", fmt.Errorf("could not create hasher: %w", err)
+ }
+
+ hash := hex.EncodeToString(hasher.ComputeHash(message))
+
+ return requiredAlgorithm, hash, nil
+}
+
+// AttachSignatures returns the given transaction with the given signatures attached to it.
+func (t *Transactor) AttachSignatures(unsigned string, signatures []object.Signature) (string, error) {
+
+ unsignedTx, err := t.decodeTransaction(unsigned)
+ if err != nil {
+ return "", fmt.Errorf("could not decode transaction: %w", err)
+ }
+
+ // Validate the transaction actors. We expect a single authorizer - the sender account.
+ if len(unsignedTx.Authorizers) != requiredAuthorizers {
+ return "", failure.InvalidAuthorizers{
+ Have: uint(len(unsignedTx.Authorizers)),
+ Want: requiredAuthorizers,
+ Description: failure.NewDescription(authorizersInvalid),
+ }
+ }
+
+ // We expect one signature for the one signer.
+ if len(unsignedTx.Authorizers) != len(signatures) {
+ return "", failure.InvalidSignatures{
+ Have: uint(len(signatures)),
+ Want: uint(len(unsignedTx.Authorizers)),
+ Description: failure.NewDescription(sigCountInvalid),
+ }
+ }
+
+ // Verify that the sender is the payer, since it is the payer that needs to sign the envelope.
+ sender := unsignedTx.Authorizers[0]
+ signature := signatures[0]
+ if unsignedTx.Payer != sender {
+ return "", failure.InvalidPayer{
+ Have: flow.BytesToAddress(unsignedTx.Payer[:]),
+ Want: flow.BytesToAddress(sender[:]),
+ Description: failure.NewDescription(payerInvalid),
+ }
+ }
+
+ // Verify that we do not already have signatures.
+ if len(unsignedTx.EnvelopeSignatures) > 0 {
+ return "", failure.InvalidSignature{
+ Description: failure.NewDescription(envelopeSigFound,
+ failure.WithInt("signatures", len(unsignedTx.EnvelopeSignatures))),
+ }
+ }
+
+ // Verify that the signature belongs to the sender.
+ signer := sdk.HexToAddress(signature.SigningPayload.AccountID.Address)
+ if signer != sender {
+ return "", failure.InvalidSignature{
+ Description: failure.NewDescription(signerInvalid,
+ failure.WithString("have_signer", signer.Hex()),
+ failure.WithString("want_signer", sender.Hex()),
+ ),
+ }
+ }
+
+ if signature.SignatureType != requiredAlgorithm {
+ return "", failure.InvalidSignature{
+ Description: failure.NewDescription(sigAlgoInvalid,
+ failure.WithString("have_algo", signature.SignatureType),
+ failure.WithString("want_algo", requiredAlgorithm),
+ ),
+ }
+ }
+
+ bytes, err := hex.DecodeString(signature.HexBytes)
+ if err != nil {
+ return "", failure.InvalidSignature{
+ Description: failure.NewDescription(sigEncoding,
+ failure.WithErr(err)),
+ }
+ }
+
+ signedTx := unsignedTx.AddEnvelopeSignature(signer, 0, bytes)
+ signed, err := t.encodeTransaction(signedTx)
+ if err != nil {
+ return "", fmt.Errorf("could not encode transaction: %w", err)
+ }
+
+ return signed, nil
+}
+
+// TransactionIdentifier returns the transaction identifier of a given signed transaction.
+func (t *Transactor) TransactionIdentifier(signed string) (identifier.Transaction, error) {
+
+ signedTx, err := t.decodeTransaction(signed)
+ if err != nil {
+ return identifier.Transaction{}, fmt.Errorf("could not decode transaction: %w", err)
+ }
+
+ rosTxID := identifier.Transaction{
+ Hash: signedTx.ID().Hex(),
+ }
+
+ return rosTxID, nil
+}
+
+// SubmitTransaction submits the given signed transaction.
+func (t *Transactor) SubmitTransaction(signed string) (identifier.Transaction, error) {
+
+ signedTx, err := t.decodeTransaction(signed)
+ if err != nil {
+ return identifier.Transaction{}, fmt.Errorf("could not decode transaction: %w", err)
+ }
+
+ err = t.submit.Transaction(signedTx)
+ if err != nil {
+ return identifier.Transaction{}, fmt.Errorf("could not submit transaction: %w", err)
+ }
+
+ return rosettaTxID(signedTx.ID()), nil
+}
+
+func (t *Transactor) encodeTransaction(tx *sdk.Transaction) (string, error) {
+
+ data, err := json.Marshal(tx)
+ if err != nil {
+ return "", fmt.Errorf("could not marshal transaction: %w", err)
+ }
+ payload := base64.StdEncoding.EncodeToString(data)
+
+ return payload, nil
+}
+
+func (t *Transactor) decodeTransaction(payload string) (*sdk.Transaction, error) {
+
+ data, err := base64.StdEncoding.DecodeString(payload)
+ if err != nil {
+ return nil, failure.InvalidPayload{
+ Description: failure.NewDescription(err.Error()),
+ Encoding: "base64",
+ }
+ }
+
+ var tx sdk.Transaction
+ err = json.Unmarshal(data, &tx)
+ if err != nil {
+ return nil, failure.InvalidPayload{
+ Description: failure.NewDescription(err.Error()),
+ Encoding: "json",
+ }
+ }
+
+ return &tx, nil
+}
+
+// Parse processes the flow transaction, validates its correctness and translates it
+// to a list of operations and a list of signers.
+func (t *Transactor) Parse(payload string) (Parser, error) {
+ tx, err := t.decodeTransaction(payload)
+ if err != nil {
+ return nil, err
+ }
+
+ p := TransactionParser{
+ tx: tx,
+ validate: t.validate,
+ generate: t.generate,
+ invoke: t.invoke,
+ }
+
+ return &p, nil
+}
diff --git a/rosetta/transactor/transactor_internal_test.go b/rosetta/transactor/transactor_internal_test.go
new file mode 100755
index 0000000..f3b87b4
--- /dev/null
+++ b/rosetta/transactor/transactor_internal_test.go
@@ -0,0 +1,64 @@
+package transactor
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+
+ "github.com/optakt/flow-rosetta/testing/mocks"
+)
+
+func TestNew(t *testing.T) {
+ validate := mocks.BaselineValidator(t)
+ generate := mocks.BaselineGenerator(t)
+ invoke := mocks.BaselineInvoker(t)
+ submit := mocks.BaselineSubmitter(t)
+
+ tr := New(validate, generate, invoke, submit)
+
+ assert.Equal(t, validate, tr.validate)
+ assert.Equal(t, generate, tr.generate)
+ assert.Equal(t, invoke, tr.invoke)
+ assert.Equal(t, submit, tr.submit)
+}
+
+func BaselineTransactor(t *testing.T, opts ...func(*Transactor)) *Transactor {
+ t.Helper()
+
+ tr := Transactor{
+ validate: mocks.BaselineValidator(t),
+ generate: mocks.BaselineGenerator(t),
+ invoke: mocks.BaselineInvoker(t),
+ submit: mocks.BaselineSubmitter(t),
+ }
+
+ for _, opt := range opts {
+ opt(&tr)
+ }
+
+ return &tr
+}
+
+func WithValidator(validator Validator) func(*Transactor) {
+ return func(transactor *Transactor) {
+ transactor.validate = validator
+ }
+}
+
+func WithGenerator(generator Generator) func(*Transactor) {
+ return func(transactor *Transactor) {
+ transactor.generate = generator
+ }
+}
+
+func WithInvoker(invoker Invoker) func(*Transactor) {
+ return func(transactor *Transactor) {
+ transactor.invoke = invoker
+ }
+}
+
+func WithSubmitter(submitter Submitter) func(*Transactor) {
+ return func(transactor *Transactor) {
+ transactor.submit = submitter
+ }
+}
diff --git a/rosetta/transactor/transactor_test.go b/rosetta/transactor/transactor_test.go
new file mode 100755
index 0000000..07b3eeb
--- /dev/null
+++ b/rosetta/transactor/transactor_test.go
@@ -0,0 +1,772 @@
+// Copyright 2021 Optakt Labs OÜ
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+package transactor_test
+
+import (
+ "encoding/base64"
+ "encoding/json"
+ "fmt"
+ "strings"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+
+ "github.com/onflow/cadence"
+ sdk "github.com/onflow/flow-go-sdk"
+ "github.com/onflow/flow-go/model/flow"
+
+ "github.com/optakt/flow-dps/models/dps"
+ "github.com/optakt/flow-rosetta/rosetta/failure"
+ "github.com/optakt/flow-rosetta/rosetta/identifier"
+ "github.com/optakt/flow-rosetta/rosetta/object"
+ "github.com/optakt/flow-rosetta/rosetta/transactor"
+ "github.com/optakt/flow-rosetta/testing/mocks"
+)
+
+func TestTransactor_DeriveIntent(t *testing.T) {
+ t.Run("nominal case", func(t *testing.T) {
+ t.Parallel()
+
+ tr := transactor.BaselineTransactor(t)
+
+ want := mocks.GenericOperations(2)
+ got, err := tr.DeriveIntent(want)
+
+ require.NoError(t, err)
+ assert.Equal(t, want[1].Amount.Value, fmt.Sprint(uint64(got.Amount)))
+ assert.Equal(t, want[1].AccountID.Address, got.To.String())
+ assert.Equal(t, want[0].AccountID.Address, got.From.String())
+ assert.Equal(t, want[0].AccountID.Address, got.Payer.String())
+ assert.Equal(t, want[0].AccountID.Address, got.Proposer.String())
+ })
+
+ t.Run("handles invalid currency", func(t *testing.T) {
+ t.Parallel()
+
+ validator := mocks.BaselineValidator(t)
+ validator.CurrencyFunc = func(identifier.Currency) (string, uint, error) {
+ return "", 0, mocks.GenericError
+ }
+
+ tr := transactor.BaselineTransactor(t, transactor.WithValidator(validator))
+
+ _, err := tr.DeriveIntent(mocks.GenericOperations(2))
+
+ assert.Error(t, err)
+ })
+
+ t.Run("handles invalid account", func(t *testing.T) {
+ t.Parallel()
+
+ validator := mocks.BaselineValidator(t)
+ validator.AccountFunc = func(account identifier.Account) (flow.Address, error) {
+ return flow.EmptyAddress, mocks.GenericError
+ }
+
+ tr := transactor.BaselineTransactor(t, transactor.WithValidator(validator))
+
+ _, err := tr.DeriveIntent(mocks.GenericOperations(2))
+
+ assert.Error(t, err)
+ })
+
+ t.Run("handles invalid number of operations", func(t *testing.T) {
+ t.Parallel()
+
+ tr := transactor.BaselineTransactor(t)
+
+ op := mocks.GenericOperations(3)
+
+ _, err := tr.DeriveIntent(op)
+
+ assert.Error(t, err)
+ assert.ErrorAs(t, err, &failure.InvalidOperations{})
+ })
+
+ t.Run("handles operations with unparsable amounts", func(t *testing.T) {
+ t.Parallel()
+
+ tr := transactor.BaselineTransactor(t)
+
+ op := mocks.GenericOperations(2)
+ op[0].Amount.Value = "42"
+ op[1].Amount.Value = "84"
+
+ _, err := tr.DeriveIntent(op)
+
+ assert.Error(t, err)
+ assert.ErrorAs(t, err, &failure.InvalidIntent{})
+ })
+
+ t.Run("handles operations with non-matching amounts", func(t *testing.T) {
+ t.Parallel()
+
+ tr := transactor.BaselineTransactor(t)
+
+ op := mocks.GenericOperations(2)
+ op[0].Amount.Value = "42"
+ op[1].Amount.Value = "84"
+
+ _, err := tr.DeriveIntent(op)
+
+ assert.Error(t, err)
+ assert.ErrorAs(t, err, &failure.InvalidIntent{})
+ })
+
+ t.Run("handles irrelevant currencies", func(t *testing.T) {
+ t.Parallel()
+
+ validator := mocks.BaselineValidator(t)
+ validator.CurrencyFunc = func(identifier.Currency) (string, uint, error) {
+ return "IRRELEVANT_CURRENCY", 0, nil
+ }
+
+ tr := transactor.BaselineTransactor(t, transactor.WithValidator(validator))
+
+ _, err := tr.DeriveIntent(mocks.GenericOperations(2))
+
+ assert.Error(t, err)
+ assert.ErrorAs(t, err, &failure.InvalidIntent{})
+ })
+
+ t.Run("handles non-transfer operations", func(t *testing.T) {
+ t.Parallel()
+
+ tr := transactor.BaselineTransactor(t)
+
+ op := mocks.GenericOperations(2)
+ op[0].Type = "irrelevant_type"
+
+ _, err := tr.DeriveIntent(op)
+
+ assert.Error(t, err)
+ assert.ErrorAs(t, err, &failure.InvalidIntent{})
+ })
+}
+
+func TestTransactor_CompileTransaction(t *testing.T) {
+ rosBlockID := mocks.GenericRosBlockID
+ sequence := uint64(42)
+
+ sender := mocks.GenericAddress(0)
+ receiver := mocks.GenericAddress(1)
+ amount, err := cadence.NewUFix64("100.00000000")
+ require.NoError(t, err)
+
+ intent := &transactor.Intent{
+ From: sender,
+ To: receiver,
+ Amount: amount,
+ Payer: sender,
+ Proposer: sender,
+ }
+
+ t.Run("nominal case", func(t *testing.T) {
+ t.Parallel()
+
+ wantCompiled := `eyJTY3JpcHQiOiJkR1Z6ZEE9PSIsIkFyZ3VtZW50cyI6WyJleUowZVhCbElqb2lWVVpwZURZMElpd2lkbUZzZFdVaU9pSXhNREF1TURBd01EQXdNREFpZlFvPSIsImV5SjBlWEJsSWpvaVFXUmtjbVZ6Y3lJc0luWmhiSFZsSWpvaU1IaGpNamd5WlRJeVl6bGlNbVExTTJObUluMEsiXSwiUmVmZXJlbmNlQmxvY2tJRCI6WzcsNDAsMjgsMTUyLDIyOSwxNCwxMzMsNzgsOCwxOCwyNTIsMTI1LDExNywyMjAsMjIwLDY2LDE3NCwxNjIsMTUxLDcxLDEyNSw4OSw5MCw0MSwyMDIsMTE1LDY5LDIxOCwyMDIsMzYsMTQzLDU0XSwiR2FzTGltaXQiOjk5OTksIlByb3Bvc2FsS2V5Ijp7IkFkZHJlc3MiOiJlNmU0NjMyYWUwMTMwOWMwIiwiS2V5SW5kZXgiOjAsIlNlcXVlbmNlTnVtYmVyIjo0Mn0sIlBheWVyIjoiZTZlNDYzMmFlMDEzMDljMCIsIkF1dGhvcml6ZXJzIjpbImU2ZTQ2MzJhZTAxMzA5YzAiXSwiUGF5bG9hZFNpZ25hdHVyZXMiOm51bGwsIkVudmVsb3BlU2lnbmF0dXJlcyI6bnVsbH0=`
+
+ generator := mocks.BaselineGenerator(t)
+ generator.TransferTokensFunc = func(symbol string) ([]byte, error) {
+ assert.Equal(t, dps.FlowSymbol, symbol)
+
+ return mocks.GenericBytes, nil
+ }
+
+ tr := transactor.BaselineTransactor(t, transactor.WithGenerator(generator))
+
+ got, err := tr.CompileTransaction(rosBlockID, intent, sequence)
+
+ require.NoError(t, err)
+ assert.Equal(t, wantCompiled, got)
+ })
+
+ t.Run("handles generator failure on TransferTokens", func(t *testing.T) {
+ t.Parallel()
+
+ generator := mocks.BaselineGenerator(t)
+ generator.TransferTokensFunc = func(string) ([]byte, error) {
+ return nil, mocks.GenericError
+ }
+
+ tr := transactor.BaselineTransactor(t, transactor.WithGenerator(generator))
+
+ _, err := tr.CompileTransaction(rosBlockID, intent, sequence)
+
+ assert.Error(t, err)
+ })
+}
+
+func TestTransactor_HashPayload(t *testing.T) {
+ header := mocks.GenericHeader
+ rosBlockID := mocks.GenericRosBlockID
+ signer := mocks.GenericAccountID(0)
+ signerAddr := mocks.GenericAddress(0)
+ tx := &sdk.Transaction{
+ ProposalKey: sdk.ProposalKey{SequenceNumber: 42},
+ }
+
+ key, err := generateKey()
+ require.NoError(t, err)
+
+ // We need to specify a weight of 1000 because otherwise multiple public keys would be required, as
+ // the total required weight for an account's signature to be considered valid has to be equal to 1000.
+ // For some reason this 1000 magic number is not exposed anywhere that I could find in Flow.
+ pubKey := key.PublicKey(1000)
+
+ data, err := json.Marshal(tx)
+ require.NoError(t, err)
+
+ payload := base64.StdEncoding.EncodeToString(data)
+
+ t.Run("nominal case", func(t *testing.T) {
+ t.Parallel()
+
+ validator := mocks.BaselineValidator(t)
+ validator.BlockFunc = func(gotBlockID identifier.Block) (uint64, flow.Identifier, error) {
+ assert.Equal(t, rosBlockID, gotBlockID)
+
+ return header.Height, header.ID(), nil
+ }
+ validator.AccountFunc = func(rosAccountID identifier.Account) (flow.Address, error) {
+ assert.Equal(t, signer, rosAccountID)
+
+ return signerAddr, nil
+ }
+
+ invoker := mocks.BaselineInvoker(t)
+ invoker.KeyFunc = func(height uint64, address flow.Address, index int) (*flow.AccountPublicKey, error) {
+ assert.Equal(t, mocks.GenericHeight, height)
+ assert.Equal(t, signerAddr, address)
+ assert.Zero(t, index)
+
+ return &pubKey, nil
+ }
+
+ tr := transactor.BaselineTransactor(
+ t,
+ transactor.WithValidator(validator),
+ transactor.WithInvoker(invoker),
+ )
+
+ algorithm, hash, err := tr.HashPayload(rosBlockID, payload, signer)
+
+ require.NoError(t, err)
+ assert.Equal(t, "ecdsa", algorithm)
+ assert.Equal(t, "3395952d355d9a5e9b5ba6f46f155cca9ec7615deef5ce1146926cb4abbf5cbb", hash)
+ })
+
+ t.Run("handles non-base64-encoded transaction payload", func(t *testing.T) {
+ t.Parallel()
+
+ tr := transactor.BaselineTransactor(t)
+
+ data, err := json.Marshal(tx)
+ require.NoError(t, err)
+
+ _, _, err = tr.HashPayload(rosBlockID, string(data), signer)
+
+ require.Error(t, err)
+ assert.ErrorAs(t, err, &failure.InvalidPayload{})
+ })
+
+ t.Run("handles non-json-encoded transaction payload", func(t *testing.T) {
+ t.Parallel()
+
+ tr := transactor.BaselineTransactor(t)
+
+ invalidPayload := base64.StdEncoding.EncodeToString(mocks.GenericBytes)
+
+ _, _, err = tr.HashPayload(rosBlockID, invalidPayload, signer)
+
+ require.Error(t, err)
+ assert.ErrorAs(t, err, &failure.InvalidPayload{})
+ })
+
+ t.Run("handles invalid block", func(t *testing.T) {
+ t.Parallel()
+
+ validator := mocks.BaselineValidator(t)
+ validator.BlockFunc = func(identifier.Block) (uint64, flow.Identifier, error) {
+ return 0, flow.ZeroID, mocks.GenericError
+ }
+
+ tr := transactor.BaselineTransactor(
+ t,
+ transactor.WithValidator(validator),
+ )
+
+ _, _, err := tr.HashPayload(rosBlockID, payload, signer)
+
+ assert.Error(t, err)
+ })
+
+ t.Run("handles invalid account", func(t *testing.T) {
+ t.Parallel()
+
+ validator := mocks.BaselineValidator(t)
+ validator.AccountFunc = func(identifier.Account) (flow.Address, error) {
+ return flow.EmptyAddress, mocks.GenericError
+ }
+
+ tr := transactor.BaselineTransactor(
+ t,
+ transactor.WithValidator(validator),
+ )
+
+ _, _, err := tr.HashPayload(rosBlockID, payload, signer)
+
+ assert.Error(t, err)
+ })
+
+ t.Run("handles invoker failure on Key", func(t *testing.T) {
+ t.Parallel()
+
+ invoker := mocks.BaselineInvoker(t)
+ invoker.KeyFunc = func(uint64, flow.Address, int) (*flow.AccountPublicKey, error) {
+ return nil, mocks.GenericError
+ }
+
+ tr := transactor.BaselineTransactor(
+ t,
+ transactor.WithInvoker(invoker),
+ )
+
+ _, _, err := tr.HashPayload(rosBlockID, payload, signer)
+
+ require.Error(t, err)
+ assert.ErrorAs(t, err, &failure.InvalidKey{})
+ })
+}
+
+func TestTransactor_Parse(t *testing.T) {
+ tx := &sdk.Transaction{
+ ProposalKey: sdk.ProposalKey{SequenceNumber: 42},
+ }
+
+ t.Run("nominal case", func(t *testing.T) {
+ t.Parallel()
+
+ data, err := json.Marshal(tx)
+ require.NoError(t, err)
+
+ encodedData := base64.StdEncoding.EncodeToString(data)
+
+ tr := transactor.BaselineTransactor(t)
+
+ got, err := tr.Parse(encodedData)
+
+ require.NoError(t, err)
+ assert.Equal(t, tx.ProposalKey.SequenceNumber, got.Sequence())
+ })
+
+ t.Run("handles missing base64 encoding", func(t *testing.T) {
+ t.Parallel()
+
+ data, err := json.Marshal(tx)
+ require.NoError(t, err)
+
+ tr := transactor.BaselineTransactor(t)
+
+ _, err = tr.Parse(string(data))
+
+ assert.Error(t, err)
+ })
+
+ t.Run("handles non-JSON-encoded payloads", func(t *testing.T) {
+ t.Parallel()
+
+ payload := mocks.GenericBytes
+
+ tr := transactor.BaselineTransactor(t)
+
+ _, err := tr.Parse(string(payload))
+
+ assert.Error(t, err)
+ })
+}
+
+func TestTransactor_AttachSignatures(t *testing.T) {
+ senderID := mocks.GenericAccountID(0)
+ sender := sdk.HexToAddress(mocks.GenericAddress(0).Hex())
+ receiverID := mocks.GenericAccountID(1)
+ receiver := sdk.HexToAddress(mocks.GenericAddress(1).Hex())
+ tx := &sdk.Transaction{
+ Authorizers: []sdk.Address{
+ sender,
+ },
+ Payer: sender,
+ }
+
+ data, err := json.Marshal(tx)
+ require.NoError(t, err)
+
+ payload := base64.StdEncoding.EncodeToString(data)
+
+ key, err := generateKey()
+ require.NoError(t, err)
+
+ // We need to specify a weight of 1000 because otherwise multiple public keys would be required, as
+ // the total required weight for an account's signature to be considered valid has to be equal to 1000.
+ // For some reason this 1000 magic number is not exposed anywhere that I could find in Flow.
+ pubKey := key.PublicKey(1000)
+ hexBytes := strings.TrimPrefix(pubKey.PublicKey.String(), "0x")
+ senderSignature := object.Signature{
+ SigningPayload: object.SigningPayload{
+ AccountID: senderID,
+ HexBytes: hexBytes,
+ SignatureType: "ecdsa",
+ },
+ SignatureType: "ecdsa",
+ HexBytes: hexBytes,
+ PublicKey: object.PublicKey{
+ HexBytes: hexBytes,
+ },
+ }
+ receiverSignature := object.Signature{
+ SigningPayload: object.SigningPayload{
+ AccountID: receiverID,
+ HexBytes: hexBytes,
+ SignatureType: "ecdsa",
+ },
+ SignatureType: "ecdsa",
+ HexBytes: hexBytes,
+ PublicKey: object.PublicKey{
+ HexBytes: hexBytes,
+ },
+ }
+ signatures := []object.Signature{senderSignature}
+
+ t.Run("nominal case", func(t *testing.T) {
+ t.Parallel()
+
+ tr := transactor.BaselineTransactor(t)
+
+ got, err := tr.AttachSignatures(payload, signatures)
+
+ require.NoError(t, err)
+ assert.NotEmpty(t, got)
+ })
+
+ t.Run("handles non-base64-encoded transaction payload", func(t *testing.T) {
+ t.Parallel()
+
+ tr := transactor.BaselineTransactor(t)
+
+ invalidPayload, err := json.Marshal(tx)
+ require.NoError(t, err)
+
+ _, err = tr.AttachSignatures(string(invalidPayload), signatures)
+
+ require.Error(t, err)
+ assert.ErrorAs(t, err, &failure.InvalidPayload{})
+ })
+
+ t.Run("handles non-json-encoded transaction payload", func(t *testing.T) {
+ t.Parallel()
+
+ tr := transactor.BaselineTransactor(t)
+
+ invalidPayload := base64.StdEncoding.EncodeToString(mocks.GenericBytes)
+
+ _, err = tr.AttachSignatures(invalidPayload, signatures)
+
+ require.Error(t, err)
+ assert.ErrorAs(t, err, &failure.InvalidPayload{})
+ })
+
+ t.Run("handles invalid number of authorizers (>)", func(t *testing.T) {
+ t.Parallel()
+
+ tr := transactor.BaselineTransactor(t)
+
+ tx := &sdk.Transaction{
+ Authorizers: []sdk.Address{
+ sender,
+ sender,
+ },
+ Payer: sender,
+ }
+
+ data, err := json.Marshal(tx)
+ require.NoError(t, err)
+
+ payload := base64.StdEncoding.EncodeToString(data)
+
+ _, err = tr.AttachSignatures(payload, signatures)
+
+ require.Error(t, err)
+ assert.ErrorAs(t, err, &failure.InvalidAuthorizers{})
+ })
+
+ t.Run("handles invalid number of authorizers (0)", func(t *testing.T) {
+ t.Parallel()
+
+ tr := transactor.BaselineTransactor(t)
+
+ tx := &sdk.Transaction{
+ Authorizers: []sdk.Address{},
+ Payer: sender,
+ }
+
+ data, err := json.Marshal(tx)
+ require.NoError(t, err)
+
+ payload := base64.StdEncoding.EncodeToString(data)
+
+ _, err = tr.AttachSignatures(payload, signatures)
+
+ require.Error(t, err)
+ assert.ErrorAs(t, err, &failure.InvalidAuthorizers{})
+ })
+
+ t.Run("handles mismatch between length of signatures and authorizers", func(t *testing.T) {
+ t.Parallel()
+
+ tr := transactor.BaselineTransactor(t)
+
+ signatures := []object.Signature{senderSignature, senderSignature, senderSignature} // 3 signatures but only 1 authorizer.
+
+ _, err = tr.AttachSignatures(payload, signatures)
+
+ require.Error(t, err)
+ assert.ErrorAs(t, err, &failure.InvalidSignatures{})
+ })
+
+ t.Run("handles mismatch between payer and sender", func(t *testing.T) {
+ t.Parallel()
+
+ tr := transactor.BaselineTransactor(t)
+
+ tx := &sdk.Transaction{
+ Authorizers: []sdk.Address{sender},
+ Payer: receiver,
+ }
+
+ data, err := json.Marshal(tx)
+ require.NoError(t, err)
+
+ payload := base64.StdEncoding.EncodeToString(data)
+
+ _, err = tr.AttachSignatures(payload, signatures)
+
+ require.Error(t, err)
+ assert.ErrorAs(t, err, &failure.InvalidPayer{})
+ })
+
+ t.Run("handles unexpected envelope signatures", func(t *testing.T) {
+ t.Parallel()
+
+ tr := transactor.BaselineTransactor(t)
+
+ tx := &sdk.Transaction{
+ Authorizers: []sdk.Address{sender},
+ Payer: sender,
+ EnvelopeSignatures: []sdk.TransactionSignature{{}}, // 1 empty senderSignature just to trigger the failure.
+ }
+
+ data, err := json.Marshal(tx)
+ require.NoError(t, err)
+
+ payload := base64.StdEncoding.EncodeToString(data)
+
+ _, err = tr.AttachSignatures(payload, signatures)
+
+ require.Error(t, err)
+ assert.ErrorAs(t, err, &failure.InvalidSignature{})
+ })
+
+ t.Run("handles mismatch between signer and sender", func(t *testing.T) {
+ t.Parallel()
+
+ tr := transactor.BaselineTransactor(t)
+
+ signatures := []object.Signature{receiverSignature} // Signed by receiver instead of sender.
+
+ _, err = tr.AttachSignatures(payload, signatures)
+
+ require.Error(t, err)
+ assert.ErrorAs(t, err, &failure.InvalidSignature{})
+ })
+
+ t.Run("handles invalid signature algorithm", func(t *testing.T) {
+ t.Parallel()
+
+ tr := transactor.BaselineTransactor(t)
+
+ signatures := []object.Signature{
+ {
+ SigningPayload: object.SigningPayload{
+ AccountID: senderID,
+ HexBytes: hexBytes,
+ SignatureType: "invalid_type",
+ },
+ SignatureType: "invalid_type",
+ HexBytes: hexBytes,
+ PublicKey: object.PublicKey{
+ HexBytes: hexBytes,
+ },
+ },
+ }
+
+ _, err = tr.AttachSignatures(payload, signatures)
+
+ require.Error(t, err)
+ assert.ErrorAs(t, err, &failure.InvalidSignature{})
+ })
+
+ t.Run("handles invalid signature bytes", func(t *testing.T) {
+ t.Parallel()
+
+ tr := transactor.BaselineTransactor(t)
+
+ signatures := []object.Signature{
+ {
+ SigningPayload: object.SigningPayload{
+ AccountID: senderID,
+ HexBytes: string(mocks.GenericBytes),
+ SignatureType: "ecdsa",
+ },
+ SignatureType: "ecdsa",
+ HexBytes: string(mocks.GenericBytes),
+ PublicKey: object.PublicKey{
+ HexBytes: string(mocks.GenericBytes),
+ },
+ },
+ }
+
+ _, err = tr.AttachSignatures(payload, signatures)
+
+ require.Error(t, err)
+ assert.ErrorAs(t, err, &failure.InvalidSignature{})
+ })
+}
+
+func TestTransactor_TransactionIdentifier(t *testing.T) {
+ tx := &sdk.Transaction{}
+
+ data, err := json.Marshal(tx)
+ require.NoError(t, err)
+
+ payload := base64.StdEncoding.EncodeToString(data)
+
+ t.Run("nominal case", func(t *testing.T) {
+ t.Parallel()
+
+ tr := transactor.BaselineTransactor(t)
+
+ got, err := tr.TransactionIdentifier(payload)
+
+ require.NoError(t, err)
+ assert.Equal(t, tx.ID().Hex(), got.Hash)
+ })
+
+ t.Run("handles non-base64-encoded transaction payload", func(t *testing.T) {
+ t.Parallel()
+
+ tr := transactor.BaselineTransactor(t)
+
+ invalidPayload, err := json.Marshal(tx)
+ require.NoError(t, err)
+
+ _, err = tr.TransactionIdentifier(string(invalidPayload))
+
+ require.Error(t, err)
+ assert.ErrorAs(t, err, &failure.InvalidPayload{})
+ })
+
+ t.Run("handles non-json-encoded transaction payload", func(t *testing.T) {
+ t.Parallel()
+
+ tr := transactor.BaselineTransactor(t)
+
+ invalidPayload := base64.StdEncoding.EncodeToString(mocks.GenericBytes)
+
+ _, err = tr.TransactionIdentifier(invalidPayload)
+
+ require.Error(t, err)
+ assert.ErrorAs(t, err, &failure.InvalidPayload{})
+ })
+}
+
+func TestTransactor_SubmitTransaction(t *testing.T) {
+ tx := &sdk.Transaction{}
+
+ data, err := json.Marshal(tx)
+ require.NoError(t, err)
+
+ payload := base64.StdEncoding.EncodeToString(data)
+
+ t.Run("nominal case", func(t *testing.T) {
+ t.Parallel()
+
+ submitter := mocks.BaselineSubmitter(t)
+ submitter.TransactionFunc = func(gotTx *sdk.Transaction) error {
+ assert.Equal(t, tx, gotTx)
+
+ return nil
+ }
+
+ tr := transactor.BaselineTransactor(t, transactor.WithSubmitter(submitter))
+
+ got, err := tr.SubmitTransaction(payload)
+
+ require.NoError(t, err)
+ assert.Equal(t, tx.ID().Hex(), got.Hash)
+ })
+
+ t.Run("handles non-base64-encoded transaction payload", func(t *testing.T) {
+ t.Parallel()
+
+ tr := transactor.BaselineTransactor(t)
+
+ invalidPayload, err := json.Marshal(tx)
+ require.NoError(t, err)
+
+ _, err = tr.SubmitTransaction(string(invalidPayload))
+
+ require.Error(t, err)
+ assert.ErrorAs(t, err, &failure.InvalidPayload{})
+ })
+
+ t.Run("handles non-json-encoded transaction payload", func(t *testing.T) {
+ t.Parallel()
+
+ tr := transactor.BaselineTransactor(t)
+
+ invalidPayload := base64.StdEncoding.EncodeToString(mocks.GenericBytes)
+
+ _, err = tr.SubmitTransaction(invalidPayload)
+
+ require.Error(t, err)
+ assert.ErrorAs(t, err, &failure.InvalidPayload{})
+ })
+
+ t.Run("handles submitter failure", func(t *testing.T) {
+ t.Parallel()
+
+ submitter := mocks.BaselineSubmitter(t)
+ submitter.TransactionFunc = func(*sdk.Transaction) error {
+ return mocks.GenericError
+ }
+
+ tr := transactor.BaselineTransactor(t, transactor.WithSubmitter(submitter))
+
+ _, err := tr.SubmitTransaction(payload)
+
+ assert.Error(t, err)
+ })
+}
diff --git a/rosetta/transactor/validator.go b/rosetta/transactor/validator.go
new file mode 100755
index 0000000..03949d2
--- /dev/null
+++ b/rosetta/transactor/validator.go
@@ -0,0 +1,28 @@
+// Copyright 2021 Optakt Labs OÜ
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+package transactor
+
+import (
+ "github.com/onflow/flow-go/model/flow"
+
+ "github.com/optakt/flow-rosetta/rosetta/identifier"
+)
+
+// Validator represents something that can validate account and block identifiers as well as currencies.
+type Validator interface {
+ Account(rosAccountID identifier.Account) (address flow.Address, err error)
+ Block(rosBlockID identifier.Block) (height uint64, blockID flow.Identifier, err error)
+ Currency(currency identifier.Currency) (symbol string, decimals uint, err error)
+}
diff --git a/rosetta/validator/account.go b/rosetta/validator/account.go
new file mode 100755
index 0000000..e07f778
--- /dev/null
+++ b/rosetta/validator/account.go
@@ -0,0 +1,54 @@
+// Copyright 2021 Optakt Labs OÜ
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+package validator
+
+import (
+ "encoding/hex"
+
+ "github.com/onflow/flow-go/model/flow"
+
+ "github.com/optakt/flow-rosetta/rosetta/failure"
+ "github.com/optakt/flow-rosetta/rosetta/identifier"
+)
+
+// Account validates the given account identifier, and if successful, returns a matching Flow Address.
+func (v *Validator) Account(account identifier.Account) (flow.Address, error) {
+
+ // Parse the address; the length was already validated, but it's still
+ // possible that the characters are not valid hex encoding.
+ bytes, err := hex.DecodeString(account.Address)
+ if err != nil {
+ return flow.EmptyAddress, failure.InvalidAccount{
+ Address: account.Address,
+ Description: failure.NewDescription(addressInvalid),
+ }
+ }
+
+ // We use the Flow chain address generator to check if the converted address
+ // is valid.
+ var address flow.Address
+ copy(address[:], bytes)
+ ok := v.params.ChainID.Chain().IsValid(address)
+ if !ok {
+ return flow.EmptyAddress, failure.InvalidAccount{
+ Address: account.Address,
+ Description: failure.NewDescription(addressMisconfigured,
+ failure.WithString("active_chain", v.params.ChainID.String()),
+ ),
+ }
+ }
+
+ return address, nil
+}
diff --git a/rosetta/validator/block.go b/rosetta/validator/block.go
new file mode 100755
index 0000000..fefa679
--- /dev/null
+++ b/rosetta/validator/block.go
@@ -0,0 +1,122 @@
+// Copyright 2021 Optakt Labs OÜ
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+package validator
+
+import (
+ "fmt"
+
+ "github.com/onflow/flow-go/model/flow"
+
+ "github.com/optakt/flow-rosetta/rosetta/failure"
+ "github.com/optakt/flow-rosetta/rosetta/identifier"
+)
+
+// Block tries to extrapolate the block identifier to a full version
+// of itself. If both index and hash are zero values, it is assumed that the
+// latest block is referenced.
+func (v *Validator) Block(rosBlockID identifier.Block) (uint64, flow.Identifier, error) {
+
+ // If both the index and the hash are missing, the block identifier is invalid, and
+ // the latest block ID is returned instead.
+ if rosBlockID.Index == nil && rosBlockID.Hash == "" {
+ last, err := v.index.Last()
+ if err != nil {
+ return 0, flow.ZeroID, fmt.Errorf("could not retrieve last: %w", err)
+ }
+ header, err := v.index.Header(last)
+ if err != nil {
+ return 0, flow.ZeroID, fmt.Errorf("could not retrieve header: %w", err)
+ }
+ return last, header.ID(), nil
+ }
+
+ // If a block hash is present, it should be a valid block ID for Flow.
+ if rosBlockID.Hash != "" {
+ _, err := flow.HexStringToIdentifier(rosBlockID.Hash)
+ if err != nil {
+ return 0, flow.ZeroID, failure.InvalidBlock{
+ Description: failure.NewDescription(blockInvalid,
+ failure.WithString("block_hash", rosBlockID.Hash),
+ ),
+ }
+ }
+ }
+
+ // If a block index is present, it should be a valid height for the DPS.
+ if rosBlockID.Index != nil {
+ first, err := v.index.First()
+ if err != nil {
+ return 0, flow.ZeroID, fmt.Errorf("could not get first: %w", err)
+ }
+ if *rosBlockID.Index < first {
+ return 0, flow.ZeroID, failure.InvalidBlock{
+ Description: failure.NewDescription(blockTooLow,
+ failure.WithUint64("block_index", *rosBlockID.Index),
+ failure.WithUint64("first_index", first),
+ ),
+ }
+ }
+ last, err := v.index.Last()
+ if err != nil {
+ return 0, flow.ZeroID, fmt.Errorf("could not get last: %w", err)
+ }
+ if *rosBlockID.Index > last {
+ return 0, flow.ZeroID, failure.UnknownBlock{
+ Index: *rosBlockID.Index,
+ Hash: rosBlockID.Hash,
+ Description: failure.NewDescription(blockTooHigh,
+ failure.WithUint64("last_index", last),
+ ),
+ }
+ }
+ }
+
+ // If we don't have a height, fill it in now.
+ if rosBlockID.Index == nil {
+ blockID, _ := flow.HexStringToIdentifier(rosBlockID.Hash)
+ height, err := v.index.HeightForBlock(blockID)
+ if err != nil {
+ return 0, flow.ZeroID, fmt.Errorf("could not get height for block: %w", err)
+ }
+ rosBlockID.Index = &height
+ }
+
+ // The given block ID should match the block ID at the given height.
+ header, err := v.index.Header(*rosBlockID.Index)
+ if err != nil {
+ return 0, flow.ZeroID, fmt.Errorf("could not get header: %w", err)
+ }
+ if rosBlockID.Hash != "" && rosBlockID.Hash != header.ID().String() {
+ return 0, flow.ZeroID, failure.InvalidBlock{
+ Description: failure.NewDescription(blockMismatch,
+ failure.WithUint64("block_index", *rosBlockID.Index),
+ failure.WithString("block_hash", rosBlockID.Hash),
+ failure.WithString("want_hash", header.ID().String()),
+ ),
+ }
+ }
+
+ return header.Height, header.ID(), nil
+}
+
+// CompleteBlockID verifies that both index and hash are populated in the block ID.
+func (v *Validator) CompleteBlockID(rosBlockID identifier.Block) error {
+ if rosBlockID.Index == nil || rosBlockID.Hash == "" {
+ return failure.IncompleteBlock{
+ Description: failure.NewDescription(blockNotFull),
+ }
+ }
+ return nil
+}
diff --git a/rosetta/validator/configuration.go b/rosetta/validator/configuration.go
new file mode 100755
index 0000000..186222d
--- /dev/null
+++ b/rosetta/validator/configuration.go
@@ -0,0 +1,23 @@
+// Copyright 2021 Optakt Labs OÜ
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+package validator
+
+import (
+ "github.com/optakt/flow-rosetta/rosetta/identifier"
+)
+
+type Configuration interface {
+ Check(identifier.Network) error
+}
diff --git a/rosetta/validator/currency.go b/rosetta/validator/currency.go
new file mode 100755
index 0000000..463be15
--- /dev/null
+++ b/rosetta/validator/currency.go
@@ -0,0 +1,52 @@
+// Copyright 2021 Optakt Labs OÜ
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+package validator
+
+import (
+ "github.com/optakt/flow-dps/models/dps"
+ "github.com/optakt/flow-rosetta/rosetta/failure"
+ "github.com/optakt/flow-rosetta/rosetta/identifier"
+)
+
+// Currency validates the given currency identifier and if it is, returns its symbol and decimals.
+func (v *Validator) Currency(currency identifier.Currency) (string, uint, error) {
+
+ // We already checked the token symbol is given, so this merely checks if
+ // the token has been configured yet.
+ _, ok := v.params.Tokens[currency.Symbol]
+ if !ok {
+ return "", 0, failure.UnknownCurrency{
+ Symbol: currency.Symbol,
+ Decimals: currency.Decimals,
+ Description: failure.NewDescription(symbolUnknown,
+ failure.WithStrings("available_symbols", v.params.Symbols()...),
+ ),
+ }
+ }
+
+ // If the token is known, there should always be 8 decimals, as we always use
+ // `UFix64` for tokens on Flow.
+ if currency.Decimals != 0 && currency.Decimals != dps.FlowDecimals {
+ return "", 0, failure.InvalidCurrency{
+ Symbol: currency.Symbol,
+ Decimals: currency.Decimals,
+ Description: failure.NewDescription(decimalsMismatch,
+ failure.WithInt("want_decimals", dps.FlowDecimals),
+ ),
+ }
+ }
+
+ return currency.Symbol, dps.FlowDecimals, nil
+}
diff --git a/rosetta/validator/errors.go b/rosetta/validator/errors.go
new file mode 100755
index 0000000..03410fe
--- /dev/null
+++ b/rosetta/validator/errors.go
@@ -0,0 +1,51 @@
+// Copyright 2021 Optakt Labs OÜ
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+package validator
+
+// Error descriptions for common errors.
+const (
+ // Network identifier errors.
+ blockchainUnknown = "network identifier has unknown blockchain field"
+ blockchainEmpty = "blockchain identifier has empty blockchain field"
+ networkUnknown = "network identifier has unknown network field"
+ networkEmpty = "blockchain identifier has empty network field"
+
+ // Block identifier errors.
+ blockInvalid = "block hash is not a valid hex-encoded string"
+ blockNotFull = "block identifier needs both fields filled for this request"
+ blockLength = "block identifier has invalid hash field length"
+ blockTooLow = "block index is below first indexed height"
+ blockTooHigh = "block index is above last indexed height"
+ blockMismatch = "block hash mismatches with authoritative hash for index"
+
+ // Account identifier errors.
+ addressEmpty = "account identifier has empty address field"
+ addressInvalid = "account address is not a valid hex-encoded string"
+ addressMisconfigured = "account address is not valid for configured chain"
+ addressLength = "account identifier has invalid address field length"
+
+ // Currency identifier errors.
+ currenciesEmpty = "currency identifier list is empty"
+ symbolEmpty = "currency identifier has empty symbol field"
+ symbolUnknown = "currency symbol is unknown"
+ decimalsMismatch = "currency decimals mismatch with authoritative decimals for symbol"
+
+ // Transaction and transaction identifier errors.
+ txHashEmpty = "transaction identifier has empty hash field"
+ txHashInvalid = "transaction hash is not a valid hex-encoded string"
+ txLength = "transaction identifier has invalid hash field length"
+ txBodyEmpty = "transaction text is empty"
+ signaturesEmpty = "signature list is empty"
+)
diff --git a/rosetta/validator/requests.go b/rosetta/validator/requests.go
new file mode 100755
index 0000000..b559bc8
--- /dev/null
+++ b/rosetta/validator/requests.go
@@ -0,0 +1,264 @@
+// Copyright 2021 Optakt Labs OÜ
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+package validator
+
+import (
+ "errors"
+
+ "github.com/go-playground/validator/v10"
+
+ "github.com/optakt/flow-rosetta/api/rosetta"
+ "github.com/optakt/flow-rosetta/rosetta/failure"
+ "github.com/optakt/flow-rosetta/rosetta/identifier"
+ "github.com/optakt/flow-rosetta/rosetta/request"
+)
+
+// ErrInvalidValidation is a sentinel error for any error not being caused by a given input failing validation.
+var ErrInvalidValidation = errors.New("invalid validation input")
+
+// Field names are displayed if the code deals with the plain `error` types instead of the structured validation errors.
+// When dealing with plain errors, the validation error reads "Field validation for 'FieldName' failed on the 'X' tag".
+// Since in our code we are dealing with structured errors, these field names are not actually used.
+// However, they are mandatory arguments for the `ReportError` method from the validator library.
+const (
+ blockHashField = "block_hash"
+ blockchainField = "blockchain"
+ networkField = "network"
+ addressField = "address"
+ txField = "transaction_id"
+ currencyField = "currency"
+ symbolField = "symbol"
+ transactionField = "transaction"
+ signaturesField = "signatures"
+
+ blockchainFailTag = "blockchain"
+ networkFailTag = "network"
+)
+
+func newRequestValidator(config Configuration) *validator.Validate {
+
+ validate := validator.New()
+
+ // Register custom validators for known types.
+ // We register a single type per validator, so we can safely perform type
+ // assertion of the provided `validator.StructLevel` to the correct type.
+ // Validation library stores the validators in a map, so if multiple
+ // validators are registered for a single type, only the last one will be used.
+ validate.RegisterStructValidation(blockValidator, identifier.Block{})
+ validate.RegisterStructValidation(accountValidator, identifier.Account{})
+ validate.RegisterStructValidation(transactionValidator, identifier.Transaction{})
+ validate.RegisterStructValidation(networkValidator(config), identifier.Network{})
+
+ // Register custom top-level validators. These validate the entire request
+ // object, compared to the ones above which validate a specific type
+ // within the request. This way we can validate some standard types (strings)
+ // or complex ones (array of currencies) in a structured way.
+ validate.RegisterStructValidation(balanceValidator, request.Balance{})
+ validate.RegisterStructValidation(parseValidator, request.Parse{})
+ validate.RegisterStructValidation(combineValidator, request.Combine{})
+ validate.RegisterStructValidation(submitValidator, request.Submit{})
+ validate.RegisterStructValidation(hashValidator, request.Hash{})
+
+ return validate
+}
+
+// Request runs the registered validators on the provided request.
+// It either returns a typed error with contextual information, or a plain error
+// describing what failed.
+func (v *Validator) Request(request interface{}) error {
+
+ err := v.validate.Struct(request)
+ // If validation passed ok - we're done.
+ if err == nil {
+ return nil
+ }
+
+ // InvalidValidationError is returned by the validation library in cases of invalid usage,
+ // more precisely, passing a non-struct to `validate.Struct()` method.
+ _, ok := err.(*validator.InvalidValidationError)
+ if ok {
+ return ErrInvalidValidation
+ }
+
+ // Process validation errors we have found. Return the first one we encounter.
+ // Validator library doesn't offer a sophisticated mechanism for passing detailed
+ // error information from the lower validation layers, so we use the `tag` field
+ // to identify what went wrong.
+ // In the case of blockchain/network misconfigurtion, we also use the `param` field
+ // to pass the acceptable value for the field.
+
+ verr := err.(validator.ValidationErrors)[0]
+
+ switch verr.Tag() {
+ case blockLength:
+ // Block hash has incorrect length.
+ blockHash, _ := verr.Value().(string)
+ return failure.InvalidBlockHash{
+ Description: failure.NewDescription(blockLength),
+ WantLength: rosetta.HexIDSize,
+ HaveLength: len(blockHash),
+ }
+
+ case addressLength:
+ // Account address has incorrect length.
+ address, _ := verr.Value().(string)
+ return failure.InvalidAccountAddress{
+ Description: failure.NewDescription(addressLength),
+ WantLength: rosetta.HexAddressSize,
+ HaveLength: len(address),
+ }
+
+ case txLength:
+ // Transaction hash has incorrect length.
+ txHash, _ := verr.Value().(string)
+ return failure.InvalidTransactionHash{
+ Description: failure.NewDescription(txLength),
+ WantLength: rosetta.HexIDSize,
+ HaveLength: len(txHash),
+ }
+
+ case networkFailTag:
+ // Network field is not referring to the network for which we are configured.
+ network, _ := verr.Value().(identifier.Network)
+ return failure.InvalidNetwork{
+ HaveNetwork: network.Network,
+ WantNetwork: verr.Param(),
+ Description: failure.NewDescription(networkUnknown),
+ }
+
+ case blockchainFailTag:
+ // Blockchain field is not referring to the blockchain for which we are configured.
+ network, _ := verr.Value().(identifier.Network)
+ return failure.InvalidBlockchain{
+ HaveBlockchain: network.Blockchain,
+ WantBlockchain: verr.Param(),
+ Description: failure.NewDescription(blockchainUnknown),
+ }
+
+ default:
+ return errors.New(verr.Tag())
+ }
+}
+
+// networkValidator verifies that the request is valid for the configured network.
+// Network and blockchain fields must be populated and valid for the current
+// running instance of the DPS. For example, a request cannot address a Testnet
+// network while we are running on top of a Mainnet index.
+func networkValidator(config Configuration) func(validator.StructLevel) {
+ return func(sl validator.StructLevel) {
+ network := sl.Current().Interface().(identifier.Network)
+
+ if network.Blockchain == "" {
+ sl.ReportError(network.Blockchain, blockchainField, blockchainField, blockchainEmpty, "")
+ }
+ if network.Network == "" {
+ sl.ReportError(network.Network, networkField, networkField, networkEmpty, "")
+ }
+
+ err := config.Check(network)
+ // Check returns a typed error, which we can use to identify which field was invalid.
+ // We will use the `tag` field to communicate this, and the `param` field to pass the acceptable
+ // value to the main validation function.
+ var ibErr failure.InvalidBlockchain
+ if errors.As(err, &ibErr) {
+ sl.ReportError(network, blockchainField, blockchainField, blockchainFailTag, ibErr.WantBlockchain)
+ }
+ var inErr failure.InvalidNetwork
+ if errors.As(err, &inErr) {
+ sl.ReportError(network, networkField, networkField, networkFailTag, inErr.WantNetwork)
+ }
+ }
+}
+
+// blockValidator ensures that, if the block hash is provided, it has the correct length.
+func blockValidator(sl validator.StructLevel) {
+ rosBlockID := sl.Current().Interface().(identifier.Block)
+ if rosBlockID.Hash != "" && len(rosBlockID.Hash) != rosetta.HexIDSize {
+ sl.ReportError(rosBlockID.Hash, blockHashField, blockHashField, blockLength, "")
+ }
+}
+
+// accountValidator ensures that the account address field is populated and has correct length.
+func accountValidator(sl validator.StructLevel) {
+ rosAccountID := sl.Current().Interface().(identifier.Account)
+ if rosAccountID.Address == "" {
+ sl.ReportError(rosAccountID.Address, addressField, addressField, addressEmpty, "")
+ }
+ if len(rosAccountID.Address) != rosetta.HexAddressSize {
+ sl.ReportError(rosAccountID.Address, addressField, addressField, addressLength, "")
+ }
+}
+
+// transactionValidator ensures that the transaction identifier is populated and has correct length.
+func transactionValidator(sl validator.StructLevel) {
+ rosTxID := sl.Current().Interface().(identifier.Transaction)
+ if rosTxID.Hash == "" {
+ sl.ReportError(rosTxID.Hash, txField, txField, txHashEmpty, "")
+ }
+ if len(rosTxID.Hash) != rosetta.HexIDSize {
+ sl.ReportError(rosTxID.Hash, txField, txField, txLength, "")
+ }
+}
+
+// balanceValidator ensures that the provided Balance request has a non-empty currency list, and that
+// all provided currencies have the `symbol` field populated.
+func balanceValidator(sl validator.StructLevel) {
+ req := sl.Current().Interface().(request.Balance)
+ if len(req.Currencies) == 0 {
+ sl.ReportError(req.Currencies, currencyField, currencyField, currenciesEmpty, "")
+ }
+ for _, currency := range req.Currencies {
+ if currency.Symbol == "" {
+ sl.ReportError(currency.Symbol, symbolField, symbolField, symbolEmpty, "")
+ }
+ }
+}
+
+// parseValidator ensures that the provided Parse request has a non-empty transaction field.
+func parseValidator(sl validator.StructLevel) {
+ req := sl.Current().Interface().(request.Parse)
+ if req.Transaction == "" {
+ sl.ReportError(req.Transaction, transactionField, transactionField, txBodyEmpty, "")
+ }
+}
+
+// combineValidator ensures that the provided Combine request has a non-empty transaction field, and
+// that the signature list is not empty.
+func combineValidator(sl validator.StructLevel) {
+ req := sl.Current().Interface().(request.Combine)
+ if req.UnsignedTransaction == "" {
+ sl.ReportError(req.UnsignedTransaction, transactionField, transactionField, txBodyEmpty, "")
+ }
+
+ if len(req.Signatures) == 0 {
+ sl.ReportError(req.Signatures, signaturesField, signaturesField, signaturesEmpty, "")
+ }
+}
+
+// submitValidator ensures that the provided Submit request has a non-empty transaction field.
+func submitValidator(sl validator.StructLevel) {
+ req := sl.Current().Interface().(request.Submit)
+ if req.SignedTransaction == "" {
+ sl.ReportError(req.SignedTransaction, transactionField, transactionField, txBodyEmpty, "")
+ }
+}
+
+// hashValidator ensures that the provided Hash request has a non-empty transaction field.
+func hashValidator(sl validator.StructLevel) {
+ req := sl.Current().Interface().(request.Hash)
+ if req.SignedTransaction == "" {
+ sl.ReportError(req.SignedTransaction, transactionField, transactionField, txBodyEmpty, "")
+ }
+}
diff --git a/rosetta/validator/transaction.go b/rosetta/validator/transaction.go
new file mode 100755
index 0000000..752588f
--- /dev/null
+++ b/rosetta/validator/transaction.go
@@ -0,0 +1,36 @@
+// Copyright 2021 Optakt Labs OÜ
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+package validator
+
+import (
+ "github.com/onflow/flow-go/model/flow"
+
+ "github.com/optakt/flow-rosetta/rosetta/failure"
+ "github.com/optakt/flow-rosetta/rosetta/identifier"
+)
+
+// Transaction validates a transaction identifier, and if its valid, returns a matching Flow Identifier.
+func (v *Validator) Transaction(transaction identifier.Transaction) (flow.Identifier, error) {
+
+ txID, err := flow.HexStringToIdentifier(transaction.Hash)
+ if err != nil {
+ return flow.ZeroID, failure.InvalidTransaction{
+ Hash: transaction.Hash,
+ Description: failure.NewDescription(txHashInvalid),
+ }
+ }
+
+ return txID, nil
+}
diff --git a/rosetta/validator/validator.go b/rosetta/validator/validator.go
new file mode 100755
index 0000000..84ff6b7
--- /dev/null
+++ b/rosetta/validator/validator.go
@@ -0,0 +1,40 @@
+// Copyright 2021 Optakt Labs OÜ
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+package validator
+
+import (
+ "github.com/go-playground/validator/v10"
+
+ "github.com/optakt/flow-dps/models/dps"
+)
+
+// Validator validates Rosetta object identifiers.
+type Validator struct {
+ params dps.Params
+ index dps.Reader
+ validate *validator.Validate
+}
+
+// New returns a new Validator.
+func New(params dps.Params, index dps.Reader, config Configuration) *Validator {
+
+ v := Validator{
+ params: params,
+ index: index,
+ validate: newRequestValidator(config),
+ }
+
+ return &v
+}
diff --git a/testing/mocks/mocks/cache.go b/testing/mocks/mocks/cache.go
new file mode 100755
index 0000000..4f9d6c9
--- /dev/null
+++ b/testing/mocks/mocks/cache.go
@@ -0,0 +1,47 @@
+// Copyright 2021 Optakt Labs OÜ
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+package mocks
+
+import (
+ "testing"
+)
+
+type Cache struct {
+ GetFunc func(key interface{}) (interface{}, bool)
+ SetFunc func(key, value interface{}, cost int64) bool
+}
+
+func BaselineCache(t *testing.T) *Cache {
+ t.Helper()
+
+ c := Cache{
+ GetFunc: func(interface{}) (interface{}, bool) {
+ return GenericBytes, true
+ },
+ SetFunc: func(interface{}, interface{}, int64) bool {
+ return true
+ },
+ }
+
+ return &c
+}
+
+func (c *Cache) Get(key interface{}) (interface{}, bool) {
+ return c.GetFunc(key)
+}
+
+func (c *Cache) Set(key, value interface{}, cost int64) bool {
+ return c.SetFunc(key, value, cost)
+}
diff --git a/testing/mocks/mocks/chain.go b/testing/mocks/mocks/chain.go
new file mode 100755
index 0000000..3baf73f
--- /dev/null
+++ b/testing/mocks/mocks/chain.go
@@ -0,0 +1,105 @@
+// Copyright 2021 Optakt Labs OÜ
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+package mocks
+
+import (
+ "testing"
+
+ "github.com/onflow/flow-go/model/flow"
+)
+
+type Chain struct {
+ RootFunc func() (uint64, error)
+ HeaderFunc func(height uint64) (*flow.Header, error)
+ CommitFunc func(height uint64) (flow.StateCommitment, error)
+ CollectionsFunc func(height uint64) ([]*flow.LightCollection, error)
+ GuaranteesFunc func(height uint64) ([]*flow.CollectionGuarantee, error)
+ TransactionsFunc func(height uint64) ([]*flow.TransactionBody, error)
+ ResultsFunc func(height uint64) ([]*flow.TransactionResult, error)
+ EventsFunc func(height uint64) ([]flow.Event, error)
+ SealsFunc func(height uint64) ([]*flow.Seal, error)
+}
+
+func BaselineChain(t *testing.T) *Chain {
+ t.Helper()
+
+ c := Chain{
+ RootFunc: func() (uint64, error) {
+ return GenericHeight, nil
+ },
+ HeaderFunc: func(height uint64) (*flow.Header, error) {
+ return GenericHeader, nil
+ },
+ CommitFunc: func(height uint64) (flow.StateCommitment, error) {
+ return GenericCommit(0), nil
+ },
+ CollectionsFunc: func(height uint64) ([]*flow.LightCollection, error) {
+ return GenericCollections(2), nil
+ },
+ GuaranteesFunc: func(height uint64) ([]*flow.CollectionGuarantee, error) {
+ return GenericGuarantees(2), nil
+ },
+ TransactionsFunc: func(height uint64) ([]*flow.TransactionBody, error) {
+ return GenericTransactions(4), nil
+ },
+ ResultsFunc: func(height uint64) ([]*flow.TransactionResult, error) {
+ return GenericResults(4), nil
+ },
+ EventsFunc: func(height uint64) ([]flow.Event, error) {
+ return GenericEvents(4), nil
+ },
+ SealsFunc: func(height uint64) ([]*flow.Seal, error) {
+ return GenericSeals(4), nil
+ },
+ }
+
+ return &c
+}
+
+func (c *Chain) Root() (uint64, error) {
+ return c.RootFunc()
+}
+
+func (c *Chain) Header(height uint64) (*flow.Header, error) {
+ return c.HeaderFunc(height)
+}
+
+func (c *Chain) Commit(height uint64) (flow.StateCommitment, error) {
+ return c.CommitFunc(height)
+}
+
+func (c *Chain) Collections(height uint64) ([]*flow.LightCollection, error) {
+ return c.CollectionsFunc(height)
+}
+
+func (c *Chain) Guarantees(height uint64) ([]*flow.CollectionGuarantee, error) {
+ return c.GuaranteesFunc(height)
+}
+
+func (c *Chain) Transactions(height uint64) ([]*flow.TransactionBody, error) {
+ return c.TransactionsFunc(height)
+}
+
+func (c *Chain) Results(height uint64) ([]*flow.TransactionResult, error) {
+ return c.ResultsFunc(height)
+}
+
+func (c *Chain) Events(height uint64) ([]flow.Event, error) {
+ return c.EventsFunc(height)
+}
+
+func (c *Chain) Seals(height uint64) ([]*flow.Seal, error) {
+ return c.SealsFunc(height)
+}
diff --git a/testing/mocks/mocks/codec.go b/testing/mocks/mocks/codec.go
new file mode 100755
index 0000000..f9af0c3
--- /dev/null
+++ b/testing/mocks/mocks/codec.go
@@ -0,0 +1,77 @@
+// Copyright 2021 Optakt Labs OÜ
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+package mocks
+
+import "testing"
+
+type Codec struct {
+ EncodeFunc func(value interface{}) ([]byte, error)
+ DecodeFunc func(data []byte, value interface{}) error
+ CompressFunc func(data []byte) ([]byte, error)
+ DecompressFunc func(compressed []byte) ([]byte, error)
+ MarshalFunc func(value interface{}) ([]byte, error)
+ UnmarshalFunc func(compressed []byte, value interface{}) error
+}
+
+func BaselineCodec(t *testing.T) *Codec {
+ t.Helper()
+
+ c := Codec{
+ EncodeFunc: func(interface{}) ([]byte, error) {
+ return GenericBytes, nil
+ },
+ DecodeFunc: func([]byte, interface{}) error {
+ return nil
+ },
+ CompressFunc: func([]byte) ([]byte, error) {
+ return GenericBytes, nil
+ },
+ DecompressFunc: func([]byte) ([]byte, error) {
+ return GenericBytes, nil
+ },
+ UnmarshalFunc: func([]byte, interface{}) error {
+ return nil
+ },
+ MarshalFunc: func(interface{}) ([]byte, error) {
+ return GenericBytes, nil
+ },
+ }
+
+ return &c
+}
+
+func (c *Codec) Encode(value interface{}) ([]byte, error) {
+ return c.EncodeFunc(value)
+}
+
+func (c *Codec) Decode(data []byte, value interface{}) error {
+ return c.DecodeFunc(data, value)
+}
+
+func (c *Codec) Compress(data []byte) ([]byte, error) {
+ return c.CompressFunc(data)
+}
+
+func (c *Codec) Decompress(data []byte) ([]byte, error) {
+ return c.DecompressFunc(data)
+}
+
+func (c *Codec) Unmarshal(b []byte, v interface{}) error {
+ return c.UnmarshalFunc(b, v)
+}
+
+func (c *Codec) Marshal(v interface{}) ([]byte, error) {
+ return c.MarshalFunc(v)
+}
diff --git a/testing/mocks/mocks/converter.go b/testing/mocks/mocks/converter.go
new file mode 100755
index 0000000..707daa5
--- /dev/null
+++ b/testing/mocks/mocks/converter.go
@@ -0,0 +1,44 @@
+// Copyright 2021 Optakt Labs OÜ
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+package mocks
+
+import (
+ "testing"
+
+ "github.com/onflow/flow-go/model/flow"
+
+ "github.com/optakt/flow-rosetta/rosetta/object"
+)
+
+type Converter struct {
+ EventToOperationFunc func(event flow.Event) (*object.Operation, error)
+}
+
+func BaselineConverter(t *testing.T) *Converter {
+ t.Helper()
+
+ c := Converter{
+ EventToOperationFunc: func(event flow.Event) (*object.Operation, error) {
+ op := GenericOperation(0)
+ return &op, nil
+ },
+ }
+
+ return &c
+}
+
+func (c *Converter) EventToOperation(event flow.Event) (transaction *object.Operation, err error) {
+ return c.EventToOperationFunc(event)
+}
diff --git a/testing/mocks/mocks/feeder.go b/testing/mocks/mocks/feeder.go
new file mode 100755
index 0000000..0a4c669
--- /dev/null
+++ b/testing/mocks/mocks/feeder.go
@@ -0,0 +1,41 @@
+// Copyright 2021 Optakt Labs OÜ
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+package mocks
+
+import (
+ "testing"
+
+ "github.com/onflow/flow-go/ledger"
+)
+
+type Feeder struct {
+ UpdateFunc func() (*ledger.TrieUpdate, error)
+}
+
+func BaselineFeeder(t *testing.T) *Feeder {
+ t.Helper()
+
+ f := Feeder{
+ UpdateFunc: func() (*ledger.TrieUpdate, error) {
+ return GenericTrieUpdate(0), nil
+ },
+ }
+
+ return &f
+}
+
+func (f *Feeder) Update() (*ledger.TrieUpdate, error) {
+ return f.UpdateFunc()
+}
diff --git a/testing/mocks/mocks/forest.go b/testing/mocks/mocks/forest.go
new file mode 100755
index 0000000..f00c164
--- /dev/null
+++ b/testing/mocks/mocks/forest.go
@@ -0,0 +1,87 @@
+// Copyright 2021 Optakt Labs OÜ
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+package mocks
+
+import (
+ "testing"
+
+ "github.com/onflow/flow-go/ledger"
+ "github.com/onflow/flow-go/ledger/complete/mtrie/trie"
+ "github.com/onflow/flow-go/model/flow"
+)
+
+type Forest struct {
+ SaveFunc func(tree *trie.MTrie, paths []ledger.Path, parent flow.StateCommitment)
+ HasFunc func(commit flow.StateCommitment) bool
+ TreeFunc func(commit flow.StateCommitment) (*trie.MTrie, bool)
+ PathsFunc func(commit flow.StateCommitment) ([]ledger.Path, bool)
+ ParentFunc func(commit flow.StateCommitment) (flow.StateCommitment, bool)
+ ResetFunc func(finalized flow.StateCommitment)
+ SizeFunc func() uint
+}
+
+func BaselineForest(t *testing.T, hasCommit bool) *Forest {
+ t.Helper()
+
+ f := Forest{
+ SaveFunc: func(tree *trie.MTrie, paths []ledger.Path, parent flow.StateCommitment) {},
+ HasFunc: func(commit flow.StateCommitment) bool {
+ return hasCommit
+ },
+ TreeFunc: func(commit flow.StateCommitment) (*trie.MTrie, bool) {
+ return GenericTrie, true
+ },
+ PathsFunc: func(commit flow.StateCommitment) ([]ledger.Path, bool) {
+ return GenericLedgerPaths(6), true
+ },
+ ParentFunc: func(commit flow.StateCommitment) (flow.StateCommitment, bool) {
+ return GenericCommit(1), true
+ },
+ ResetFunc: func(finalized flow.StateCommitment) {},
+ SizeFunc: func() uint {
+ return 42
+ },
+ }
+
+ return &f
+}
+
+func (f *Forest) Save(tree *trie.MTrie, paths []ledger.Path, parent flow.StateCommitment) {
+ f.SaveFunc(tree, paths, parent)
+}
+
+func (f *Forest) Has(commit flow.StateCommitment) bool {
+ return f.HasFunc(commit)
+}
+
+func (f *Forest) Tree(commit flow.StateCommitment) (*trie.MTrie, bool) {
+ return f.TreeFunc(commit)
+}
+
+func (f *Forest) Paths(commit flow.StateCommitment) ([]ledger.Path, bool) {
+ return f.PathsFunc(commit)
+}
+
+func (f *Forest) Parent(commit flow.StateCommitment) (flow.StateCommitment, bool) {
+ return f.ParentFunc(commit)
+}
+
+func (f *Forest) Reset(finalized flow.StateCommitment) {
+ f.ResetFunc(finalized)
+}
+
+func (f *Forest) Size() uint {
+ return f.SizeFunc()
+}
diff --git a/testing/mocks/mocks/generator.go b/testing/mocks/mocks/generator.go
new file mode 100755
index 0000000..40814a0
--- /dev/null
+++ b/testing/mocks/mocks/generator.go
@@ -0,0 +1,61 @@
+// Copyright 2021 Optakt Labs OÜ
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+package mocks
+
+import "testing"
+
+type Generator struct {
+ GetBalanceFunc func(symbol string) ([]byte, error)
+ TokensDepositedFunc func(symbol string) (string, error)
+ TokensWithdrawnFunc func(symbol string) (string, error)
+ TransferTokensFunc func(symbol string) ([]byte, error)
+}
+
+func BaselineGenerator(t *testing.T) *Generator {
+ t.Helper()
+
+ g := Generator{
+ GetBalanceFunc: func(string) ([]byte, error) {
+ return []byte(GenericAmount(0).String()), nil
+ },
+ TokensDepositedFunc: func(string) (string, error) {
+ return string(GenericEventType(0)), nil
+ },
+ TokensWithdrawnFunc: func(string) (string, error) {
+ return string(GenericEventType(1)), nil
+ },
+ TransferTokensFunc: func(string) ([]byte, error) {
+ return GenericBytes, nil
+ },
+ }
+
+ return &g
+}
+
+func (g *Generator) GetBalance(symbol string) ([]byte, error) {
+ return g.GetBalanceFunc(symbol)
+}
+
+func (g *Generator) TokensDeposited(symbol string) (string, error) {
+ return g.TokensDepositedFunc(symbol)
+}
+
+func (g *Generator) TokensWithdrawn(symbol string) (string, error) {
+ return g.TokensWithdrawnFunc(symbol)
+}
+
+func (g *Generator) TransferTokens(symbol string) ([]byte, error) {
+ return g.TransferTokensFunc(symbol)
+}
diff --git a/testing/mocks/mocks/generic.go b/testing/mocks/mocks/generic.go
new file mode 100755
index 0000000..813b98b
--- /dev/null
+++ b/testing/mocks/mocks/generic.go
@@ -0,0 +1,609 @@
+// Copyright 2021 Optakt Labs OÜ
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+package mocks
+
+import (
+ "encoding/binary"
+ "errors"
+ "fmt"
+ "io"
+ "math/rand"
+ "time"
+
+ "github.com/rs/zerolog"
+
+ "github.com/onflow/cadence"
+ "github.com/onflow/cadence/encoding/json"
+ "github.com/onflow/cadence/runtime/tests/utils"
+ "github.com/onflow/flow-go/crypto"
+ chash "github.com/onflow/flow-go/crypto/hash"
+ "github.com/onflow/flow-go/engine/execution/computation/computer/uploader"
+ "github.com/onflow/flow-go/ledger"
+ "github.com/onflow/flow-go/ledger/common/hash"
+ "github.com/onflow/flow-go/ledger/complete/mtrie/node"
+ "github.com/onflow/flow-go/ledger/complete/mtrie/trie"
+ "github.com/onflow/flow-go/model/flow"
+ "github.com/onflow/flow-go/module/mempool/entity"
+
+ "github.com/optakt/flow-dps/models/dps"
+ "github.com/optakt/flow-rosetta/rosetta/identifier"
+ "github.com/optakt/flow-rosetta/rosetta/object"
+)
+
+// Offsets used to ensure different flow identifiers that do not overlap.
+// Each resource type has a range of 16 unique identifiers at the moment.
+// We can increase this range if we need to, in the future.
+const (
+ offsetBlock = 0
+ offsetCollection = 1 * 16
+ offsetResult = 2 * 16
+)
+
+// Global variables that can be used for testing. They are non-nil valid values for the types commonly needed
+// test DPS components.
+var (
+ NoopLogger = zerolog.New(io.Discard)
+
+ GenericError = errors.New("dummy error")
+
+ GenericHeight = uint64(42)
+
+ GenericBytes = []byte(`test`)
+
+ GenericHeader = &flow.Header{
+ ChainID: dps.FlowTestnet,
+ Height: GenericHeight,
+ ParentID: genericIdentifier(0, offsetBlock),
+ Timestamp: time.Date(1972, 11, 12, 13, 14, 15, 16, time.UTC),
+ }
+
+ GenericLedgerKey = ledger.NewKey([]ledger.KeyPart{
+ ledger.NewKeyPart(0, []byte(`owner`)),
+ ledger.NewKeyPart(1, []byte(`controller`)),
+ ledger.NewKeyPart(2, []byte(`key`)),
+ })
+
+ // GenericRootNode Visual Representation:
+ // 6 (root)
+ // / \
+ // 3 5
+ // / \ \
+ // 1 2 4
+ GenericRootNode = node.NewNode(
+ 256,
+ node.NewNode(
+ 256,
+ node.NewLeaf(GenericLedgerPath(0), GenericLedgerPayload(0), 42),
+ node.NewLeaf(GenericLedgerPath(1), GenericLedgerPayload(1), 42),
+ GenericLedgerPath(2),
+ GenericLedgerPayload(2),
+ hash.DummyHash,
+ 64,
+ 64,
+ ),
+ node.NewNode(
+ 256,
+ node.NewLeaf(GenericLedgerPath(3), GenericLedgerPayload(3), 42),
+ nil,
+ GenericLedgerPath(4),
+ GenericLedgerPayload(4),
+ hash.DummyHash,
+ 64,
+ 64,
+ ),
+ GenericLedgerPath(5),
+ GenericLedgerPayload(5),
+ hash.DummyHash,
+ 64,
+ 64,
+ )
+
+ GenericTrie, _ = trie.NewMTrie(GenericRootNode)
+
+ GenericCurrency = identifier.Currency{
+ Symbol: dps.FlowSymbol,
+ Decimals: dps.FlowDecimals,
+ }
+ GenericAccount = flow.Account{
+ Address: GenericAddress(0),
+ Balance: 84,
+ Keys: []flow.AccountPublicKey{
+ {
+ Index: 0,
+ SeqNumber: 42,
+ HashAlgo: chash.SHA2_256,
+ PublicKey: crypto.NeutralBLSPublicKey(),
+ },
+ },
+ }
+
+ GenericRosBlockID = identifier.Block{
+ Index: &GenericHeight,
+ Hash: GenericHeader.ID().String(),
+ }
+
+ GenericParams = dps.Params{ChainID: dps.FlowTestnet}
+)
+
+func GenericBlockIDs(number int) []flow.Identifier {
+ return genericIdentifiers(number, offsetBlock)
+}
+
+func GenericCommits(number int) []flow.StateCommitment {
+ // Ensure consistent deterministic results.
+ random := rand.New(rand.NewSource(0))
+
+ var commits []flow.StateCommitment
+ for i := 0; i < number; i++ {
+ var c flow.StateCommitment
+ binary.BigEndian.PutUint64(c[0:], random.Uint64())
+ binary.BigEndian.PutUint64(c[8:], random.Uint64())
+ binary.BigEndian.PutUint64(c[16:], random.Uint64())
+ binary.BigEndian.PutUint64(c[24:], random.Uint64())
+
+ commits = append(commits, c)
+ }
+
+ return commits
+}
+
+func GenericCommit(index int) flow.StateCommitment {
+ return GenericCommits(index + 1)[index]
+}
+
+func GenericTrieUpdates(number int) []*ledger.TrieUpdate {
+ // Ensure consistent deterministic results.
+ seed := rand.NewSource(1)
+ random := rand.New(seed)
+
+ var updates []*ledger.TrieUpdate
+ for i := 0; i < number; i++ {
+ update := ledger.TrieUpdate{
+ Paths: GenericLedgerPaths(6),
+ Payloads: GenericLedgerPayloads(6),
+ }
+
+ _, _ = random.Read(update.RootHash[:])
+ // To be a valid RootHash it needs to start with 0x00 0x20, which is a 16 bit uint
+ // with a value of 32, which represents its length.
+ update.RootHash[0] = 0x00
+ update.RootHash[1] = 0x20
+
+ updates = append(updates, &update)
+ }
+
+ return updates
+}
+
+func GenericTrieUpdate(index int) *ledger.TrieUpdate {
+ return GenericTrieUpdates(index + 1)[index]
+}
+
+func GenericLedgerPaths(number int) []ledger.Path {
+ // Ensure consistent deterministic results.
+ seed := rand.NewSource(2)
+ random := rand.New(seed)
+
+ var paths []ledger.Path
+ for i := 0; i < number; i++ {
+ var path ledger.Path
+ binary.BigEndian.PutUint64(path[0:], random.Uint64())
+ binary.BigEndian.PutUint64(path[8:], random.Uint64())
+ binary.BigEndian.PutUint64(path[16:], random.Uint64())
+ binary.BigEndian.PutUint64(path[24:], random.Uint64())
+
+ paths = append(paths, path)
+ }
+
+ return paths
+}
+
+func GenericLedgerPath(index int) ledger.Path {
+ return GenericLedgerPaths(index + 1)[index]
+}
+
+func GenericLedgerValues(number int) []ledger.Value {
+ // Ensure consistent deterministic results.
+ random := rand.New(rand.NewSource(3))
+
+ var values []ledger.Value
+ for i := 0; i < number; i++ {
+ value := make(ledger.Value, 32)
+ binary.BigEndian.PutUint64(value[0:], random.Uint64())
+ binary.BigEndian.PutUint64(value[8:], random.Uint64())
+ binary.BigEndian.PutUint64(value[16:], random.Uint64())
+ binary.BigEndian.PutUint64(value[24:], random.Uint64())
+
+ values = append(values, value)
+ }
+
+ return values
+}
+
+func GenericLedgerValue(index int) ledger.Value {
+ return GenericLedgerValues(index + 1)[index]
+}
+
+func GenericLedgerPayloads(number int) []*ledger.Payload {
+ var payloads []*ledger.Payload
+ for i := 0; i < number; i++ {
+ payloads = append(payloads, ledger.NewPayload(GenericLedgerKey, GenericLedgerValue(i)))
+ }
+
+ return payloads
+}
+
+func GenericLedgerPayload(index int) *ledger.Payload {
+ return GenericLedgerPayloads(index + 1)[index]
+}
+
+func GenericTransactions(number int) []*flow.TransactionBody {
+ var txs []*flow.TransactionBody
+ for i := 0; i < number; i++ {
+ tx := flow.TransactionBody{
+ ReferenceBlockID: genericIdentifier(i, offsetBlock),
+ }
+ txs = append(txs, &tx)
+ }
+
+ return txs
+}
+
+func GenericTransactionIDs(number int) []flow.Identifier {
+ transactions := GenericTransactions(number)
+
+ var txIDs []flow.Identifier
+ for _, tx := range transactions {
+ txIDs = append(txIDs, tx.ID())
+ }
+
+ return txIDs
+}
+
+func GenericTransaction(index int) *flow.TransactionBody {
+ return GenericTransactions(index + 1)[index]
+}
+
+func GenericEventTypes(number int) []flow.EventType {
+ // Ensure consistent deterministic results.
+ random := rand.New(rand.NewSource(4))
+
+ var types []flow.EventType
+ for i := 0; i < number; i++ {
+ types = append(types, flow.EventType(fmt.Sprint(random.Int())))
+ }
+
+ return types
+}
+
+func GenericEventType(index int) flow.EventType {
+ return GenericEventTypes(index + 1)[index]
+}
+
+func GenericCadenceEventTypes(number int) []*cadence.EventType {
+ var types []*cadence.EventType
+ for i := 0; i < number; i++ {
+ types = append(types, &cadence.EventType{
+ Location: utils.TestLocation,
+ QualifiedIdentifier: string(GenericEventType(i)),
+ Fields: []cadence.Field{
+ {
+ Identifier: "amount",
+ Type: cadence.UInt64Type{},
+ },
+ {
+ Identifier: "address",
+ Type: cadence.AddressType{},
+ },
+ },
+ })
+ }
+
+ return types
+}
+
+func GenericCadenceEventType(index int) *cadence.EventType {
+ return GenericCadenceEventTypes(index + 1)[index]
+}
+
+func GenericAddresses(number int) []flow.Address {
+ // Ensure consistent deterministic results.
+ random := rand.New(rand.NewSource(5))
+
+ var addresses []flow.Address
+ for i := 0; i < number; i++ {
+ var address flow.Address
+ binary.BigEndian.PutUint64(address[0:], random.Uint64())
+
+ addresses = append(addresses, address)
+ }
+
+ return addresses
+}
+
+func GenericAddress(index int) flow.Address {
+ return GenericAddresses(index + 1)[index]
+}
+
+func GenericAccountID(index int) identifier.Account {
+ return identifier.Account{Address: GenericAddress(index).String()}
+}
+
+func GenericCadenceEvents(number int) []cadence.Event {
+ // Ensure consistent deterministic results.
+ random := rand.New(rand.NewSource(6))
+
+ var events []cadence.Event
+ for i := 0; i < number; i++ {
+
+ // We want only two types of events to simulate deposit/withdrawal.
+ eventType := GenericCadenceEventType(i % 2)
+
+ event := cadence.NewEvent(
+ []cadence.Value{
+ cadence.NewUInt64(random.Uint64()),
+ cadence.NewAddress(GenericAddress(i)),
+ },
+ ).WithType(eventType)
+
+ events = append(events, event)
+ }
+
+ return events
+}
+
+func GenericCadenceEvent(index int) cadence.Event {
+ return GenericCadenceEvents(index + 1)[index]
+}
+
+func GenericEvents(number int, types ...flow.EventType) []flow.Event {
+ txIDs := GenericTransactionIDs(number)
+
+ var events []flow.Event
+ for i := 0; i < number; i++ {
+
+ // If types were provided, alternate between them. Otherwise, assume a type of 0.
+ eventType := GenericEventType(0)
+ if len(types) != 0 {
+ typeIdx := i % len(types)
+ eventType = types[typeIdx]
+ }
+
+ event := flow.Event{
+ // We want each pair of events to be related to a single transaction.
+ TransactionID: txIDs[i],
+ EventIndex: uint32(i),
+ Type: eventType,
+ Payload: json.MustEncode(GenericCadenceEvent(i)),
+ }
+
+ events = append(events, event)
+ }
+
+ return events
+}
+
+func GenericEvent(index int) flow.Event {
+ return GenericEvents(index + 1)[index]
+}
+
+func GenericTransactionQualifier(index int) identifier.Transaction {
+ txID := GenericTransaction(index).ID()
+ return identifier.Transaction{Hash: txID.String()}
+}
+
+func GenericOperations(number int) []object.Operation {
+ var operations []object.Operation
+ for i := 0; i < number; i++ {
+ // We want only two accounts to simulate transactions between them.
+ account := GenericAccountID(i % 2)
+
+ // Simulate that every second operation is the withdrawal.
+ value := GenericAmount(i / 2).String()
+ if i%2 == 1 {
+ value = "-" + value
+ }
+
+ operation := object.Operation{
+ ID: identifier.Operation{Index: uint(i)},
+ Type: dps.OperationTransfer,
+ Status: dps.StatusCompleted,
+ AccountID: account,
+ Amount: object.Amount{
+ Value: value,
+ Currency: GenericCurrency,
+ },
+ }
+
+ operations = append(operations, operation)
+ }
+
+ return operations
+}
+
+func GenericOperation(index int) object.Operation {
+ return GenericOperations(index + 1)[index]
+}
+
+func GenericCollections(number int) []*flow.LightCollection {
+ txIDs := GenericTransactionIDs(number * 2)
+
+ var collections []*flow.LightCollection
+ for i := 0; i < number; i++ {
+ collections = append(collections, &flow.LightCollection{Transactions: txIDs[i*2 : i*2+2]})
+ }
+
+ return collections
+}
+
+func GenericCollectionIDs(number int) []flow.Identifier {
+ collections := GenericCollections(number)
+
+ var collIDs []flow.Identifier
+ for _, collection := range collections {
+ collIDs = append(collIDs, collection.ID())
+ }
+
+ return collIDs
+}
+
+func GenericCollection(index int) *flow.LightCollection {
+ return GenericCollections(index + 1)[index]
+}
+
+func GenericGuarantees(number int) []*flow.CollectionGuarantee {
+ var guarantees []*flow.CollectionGuarantee
+ for i := 0; i < number; i++ {
+ j := i * 2
+ guarantees = append(guarantees, &flow.CollectionGuarantee{
+ CollectionID: genericIdentifier(i, offsetCollection),
+ ReferenceBlockID: genericIdentifier(j, offsetBlock),
+ Signature: GenericBytes,
+ })
+ }
+
+ return guarantees
+}
+
+func GenericGuarantee(index int) *flow.CollectionGuarantee {
+ return GenericGuarantees(index + 1)[index]
+}
+
+func GenericResults(number int) []*flow.TransactionResult {
+ var results []*flow.TransactionResult
+ for i := 0; i < number; i++ {
+ results = append(results, &flow.TransactionResult{
+ TransactionID: genericIdentifier(i, offsetResult),
+ })
+ }
+
+ return results
+}
+
+func GenericResult(index int) *flow.TransactionResult {
+ return GenericResults(index + 1)[index]
+}
+
+func GenericAmount(delta int) cadence.Value {
+ // Ensure consistent deterministic results.
+ random := rand.New(rand.NewSource(int64(delta)))
+
+ return cadence.NewUInt64(random.Uint64())
+}
+
+func GenericSeals(number int) []*flow.Seal {
+ var seals []*flow.Seal
+ for i := 0; i < number; i++ {
+
+ // Since we need two identifiers per seal (for BlockID and ResultID),
+ // we'll use a secondary index.
+ j := 2 * i
+
+ seal := flow.Seal{
+ BlockID: genericIdentifier(j, offsetBlock),
+ ResultID: genericIdentifier(j+1, offsetResult),
+ FinalState: GenericCommit(i),
+
+ AggregatedApprovalSigs: nil,
+ ServiceEvents: nil,
+ }
+
+ seals = append(seals, &seal)
+ }
+
+ return seals
+}
+
+func GenericSealIDs(number int) []flow.Identifier {
+ seals := GenericSeals(number)
+
+ var sealIDs []flow.Identifier
+ for _, seal := range seals {
+ sealIDs = append(sealIDs, seal.ID())
+ }
+
+ return sealIDs
+}
+
+func GenericSeal(index int) *flow.Seal {
+ return GenericSeals(index + 1)[index]
+}
+
+func GenericRecord() *uploader.BlockData {
+ var collections []*entity.CompleteCollection
+ for _, guarantee := range GenericGuarantees(4) {
+ collections = append(collections, &entity.CompleteCollection{
+ Guarantee: guarantee,
+ Transactions: GenericTransactions(2),
+ })
+ }
+
+ var events []*flow.Event
+ for _, event := range GenericEvents(4) {
+ events = append(events, &event)
+ }
+
+ data := uploader.BlockData{
+ Block: &flow.Block{
+ Header: GenericHeader,
+ Payload: &flow.Payload{
+ Guarantees: GenericGuarantees(4),
+ Seals: GenericSeals(4),
+ },
+ },
+ Collections: collections,
+ TxResults: GenericResults(4),
+ Events: events,
+ TrieUpdates: GenericTrieUpdates(4),
+ FinalStateCommitment: GenericCommit(0),
+ }
+
+ return &data
+}
+
+func ByteSlice(v interface{}) []byte {
+ switch vv := v.(type) {
+ case ledger.Path:
+ return vv[:]
+ case flow.Identifier:
+ return vv[:]
+ case flow.StateCommitment:
+ return vv[:]
+ default:
+ panic("invalid type")
+ }
+}
+
+func genericIdentifiers(number, offset int) []flow.Identifier {
+ // Ensure consistent deterministic results.
+ random := rand.New(rand.NewSource(1 + int64(offset*10)))
+
+ var ids []flow.Identifier
+ for i := 0; i < number; i++ {
+ var id flow.Identifier
+ binary.BigEndian.PutUint64(id[0:], random.Uint64())
+ binary.BigEndian.PutUint64(id[8:], random.Uint64())
+ binary.BigEndian.PutUint64(id[16:], random.Uint64())
+ binary.BigEndian.PutUint64(id[24:], random.Uint64())
+
+ ids = append(ids, id)
+ }
+
+ return ids
+}
+
+func genericIdentifier(index, offset int) flow.Identifier {
+ return genericIdentifiers(index+1, offset)[index]
+}
diff --git a/testing/mocks/mocks/invoker.go b/testing/mocks/mocks/invoker.go
new file mode 100755
index 0000000..db5d8e3
--- /dev/null
+++ b/testing/mocks/mocks/invoker.go
@@ -0,0 +1,58 @@
+// Copyright 2021 Optakt Labs OÜ
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+package mocks
+
+import (
+ "testing"
+
+ "github.com/onflow/cadence"
+ "github.com/onflow/flow-go/model/flow"
+)
+
+type Invoker struct {
+ KeyFunc func(height uint64, address flow.Address, index int) (*flow.AccountPublicKey, error)
+ AccountFunc func(height uint64, address flow.Address) (*flow.Account, error)
+ ScriptFunc func(height uint64, script []byte, parameters []cadence.Value) (cadence.Value, error)
+}
+
+func BaselineInvoker(t *testing.T) *Invoker {
+ t.Helper()
+
+ i := Invoker{
+ KeyFunc: func(height uint64, address flow.Address, index int) (*flow.AccountPublicKey, error) {
+ return &GenericAccount.Keys[0], nil
+ },
+ AccountFunc: func(height uint64, address flow.Address) (*flow.Account, error) {
+ return &GenericAccount, nil
+ },
+ ScriptFunc: func(height uint64, script []byte, parameters []cadence.Value) (cadence.Value, error) {
+ return GenericAmount(0), nil
+ },
+ }
+
+ return &i
+}
+
+func (i *Invoker) Key(height uint64, address flow.Address, index int) (*flow.AccountPublicKey, error) {
+ return i.KeyFunc(height, address, index)
+}
+
+func (i *Invoker) Account(height uint64, address flow.Address) (*flow.Account, error) {
+ return i.AccountFunc(height, address)
+}
+
+func (i *Invoker) Script(height uint64, script []byte, parameters []cadence.Value) (cadence.Value, error) {
+ return i.ScriptFunc(height, script, parameters)
+}
diff --git a/testing/mocks/mocks/loader.go b/testing/mocks/mocks/loader.go
new file mode 100755
index 0000000..071f33d
--- /dev/null
+++ b/testing/mocks/mocks/loader.go
@@ -0,0 +1,41 @@
+// Copyright 2021 Optakt Labs OÜ
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+package mocks
+
+import (
+ "testing"
+
+ "github.com/onflow/flow-go/ledger/complete/mtrie/trie"
+)
+
+type Loader struct {
+ CheckpointFunc func() (*trie.MTrie, error)
+}
+
+func BaselineLoader(t *testing.T) *Loader {
+ t.Helper()
+
+ l := Loader{
+ CheckpointFunc: func() (*trie.MTrie, error) {
+ return GenericTrie, nil
+ },
+ }
+
+ return &l
+}
+
+func (l *Loader) Checkpoint() (*trie.MTrie, error) {
+ return l.CheckpointFunc()
+}
diff --git a/testing/mocks/mocks/reader.go b/testing/mocks/mocks/reader.go
new file mode 100755
index 0000000..405b463
--- /dev/null
+++ b/testing/mocks/mocks/reader.go
@@ -0,0 +1,162 @@
+// Copyright 2021 Optakt Labs OÜ
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+package mocks
+
+import (
+ "testing"
+
+ "github.com/onflow/flow-go/ledger"
+ "github.com/onflow/flow-go/model/flow"
+)
+
+type Reader struct {
+ FirstFunc func() (uint64, error)
+ LastFunc func() (uint64, error)
+ HeightForBlockFunc func(blockID flow.Identifier) (uint64, error)
+ CommitFunc func(height uint64) (flow.StateCommitment, error)
+ HeaderFunc func(height uint64) (*flow.Header, error)
+ EventsFunc func(height uint64, types ...flow.EventType) ([]flow.Event, error)
+ ValuesFunc func(height uint64, paths []ledger.Path) ([]ledger.Value, error)
+ CollectionFunc func(collID flow.Identifier) (*flow.LightCollection, error)
+ CollectionsByHeightFunc func(height uint64) ([]flow.Identifier, error)
+ GuaranteeFunc func(collID flow.Identifier) (*flow.CollectionGuarantee, error)
+ TransactionFunc func(txID flow.Identifier) (*flow.TransactionBody, error)
+ HeightForTransactionFunc func(txID flow.Identifier) (uint64, error)
+ TransactionsByHeightFunc func(height uint64) ([]flow.Identifier, error)
+ ResultFunc func(txID flow.Identifier) (*flow.TransactionResult, error)
+ SealFunc func(sealID flow.Identifier) (*flow.Seal, error)
+ SealsByHeightFunc func(height uint64) ([]flow.Identifier, error)
+}
+
+func BaselineReader(t *testing.T) *Reader {
+ t.Helper()
+
+ r := Reader{
+ FirstFunc: func() (uint64, error) {
+ return GenericHeight, nil
+ },
+ LastFunc: func() (uint64, error) {
+ return GenericHeight, nil
+ },
+ HeightForBlockFunc: func(blockID flow.Identifier) (uint64, error) {
+ return GenericHeight, nil
+ },
+ CommitFunc: func(height uint64) (flow.StateCommitment, error) {
+ return GenericCommit(0), nil
+ },
+ HeaderFunc: func(height uint64) (*flow.Header, error) {
+ return GenericHeader, nil
+ },
+ EventsFunc: func(height uint64, types ...flow.EventType) ([]flow.Event, error) {
+ return GenericEvents(4, GenericEventTypes(2)...), nil
+ },
+ ValuesFunc: func(height uint64, paths []ledger.Path) ([]ledger.Value, error) {
+ return GenericLedgerValues(6), nil
+ },
+ CollectionFunc: func(collID flow.Identifier) (*flow.LightCollection, error) {
+ return GenericCollection(0), nil
+ },
+ CollectionsByHeightFunc: func(height uint64) ([]flow.Identifier, error) {
+ return GenericCollectionIDs(5), nil
+ },
+ GuaranteeFunc: func(collID flow.Identifier) (*flow.CollectionGuarantee, error) {
+ return GenericGuarantee(0), nil
+ },
+ TransactionFunc: func(txID flow.Identifier) (*flow.TransactionBody, error) {
+ return GenericTransaction(0), nil
+ },
+ HeightForTransactionFunc: func(blockID flow.Identifier) (uint64, error) {
+ return GenericHeight, nil
+ },
+ TransactionsByHeightFunc: func(height uint64) ([]flow.Identifier, error) {
+ return GenericTransactionIDs(5), nil
+ },
+ ResultFunc: func(txID flow.Identifier) (*flow.TransactionResult, error) {
+ return GenericResult(0), nil
+ },
+ SealFunc: func(sealID flow.Identifier) (*flow.Seal, error) {
+ return GenericSeal(0), nil
+ },
+ SealsByHeightFunc: func(height uint64) ([]flow.Identifier, error) {
+ return GenericSealIDs(5), nil
+ },
+ }
+
+ return &r
+}
+
+func (r *Reader) First() (uint64, error) {
+ return r.FirstFunc()
+}
+
+func (r *Reader) Last() (uint64, error) {
+ return r.LastFunc()
+}
+
+func (r *Reader) HeightForBlock(blockID flow.Identifier) (uint64, error) {
+ return r.HeightForBlockFunc(blockID)
+}
+
+func (r *Reader) Commit(height uint64) (flow.StateCommitment, error) {
+ return r.CommitFunc(height)
+}
+
+func (r *Reader) Header(height uint64) (*flow.Header, error) {
+ return r.HeaderFunc(height)
+}
+
+func (r *Reader) Events(height uint64, types ...flow.EventType) ([]flow.Event, error) {
+ return r.EventsFunc(height, types...)
+}
+
+func (r *Reader) Values(height uint64, paths []ledger.Path) ([]ledger.Value, error) {
+ return r.ValuesFunc(height, paths)
+}
+
+func (r *Reader) Collection(collID flow.Identifier) (*flow.LightCollection, error) {
+ return r.CollectionFunc(collID)
+}
+
+func (r *Reader) CollectionsByHeight(height uint64) ([]flow.Identifier, error) {
+ return r.CollectionsByHeightFunc(height)
+}
+
+func (r *Reader) Guarantee(collID flow.Identifier) (*flow.CollectionGuarantee, error) {
+ return r.GuaranteeFunc(collID)
+}
+
+func (r *Reader) Transaction(txID flow.Identifier) (*flow.TransactionBody, error) {
+ return r.TransactionFunc(txID)
+}
+
+func (r *Reader) HeightForTransaction(txID flow.Identifier) (uint64, error) {
+ return r.HeightForTransactionFunc(txID)
+}
+
+func (r *Reader) TransactionsByHeight(height uint64) ([]flow.Identifier, error) {
+ return r.TransactionsByHeightFunc(height)
+}
+
+func (r *Reader) Result(txID flow.Identifier) (*flow.TransactionResult, error) {
+ return r.ResultFunc(txID)
+}
+
+func (r *Reader) Seal(sealID flow.Identifier) (*flow.Seal, error) {
+ return r.SealFunc(sealID)
+}
+
+func (r *Reader) SealsByHeight(height uint64) ([]flow.Identifier, error) {
+ return r.SealsByHeightFunc(height)
+}
diff --git a/testing/mocks/mocks/record_holder.go b/testing/mocks/mocks/record_holder.go
new file mode 100755
index 0000000..8cfe424
--- /dev/null
+++ b/testing/mocks/mocks/record_holder.go
@@ -0,0 +1,71 @@
+// Copyright 2021 Optakt Labs OÜ
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+package mocks
+
+import (
+ "testing"
+
+ "github.com/onflow/flow-go/engine/execution/computation/computer/uploader"
+ "github.com/onflow/flow-go/model/flow"
+ "github.com/onflow/flow-go/module/mempool/entity"
+)
+
+type RecordHolder struct {
+ RecordFunc func(blockID flow.Identifier) (*uploader.BlockData, error)
+}
+
+func BaselineRecordHolder(t *testing.T) *RecordHolder {
+ t.Helper()
+
+ r := RecordHolder{
+ RecordFunc: func(flow.Identifier) (*uploader.BlockData, error) {
+ var collections []*entity.CompleteCollection
+ for _, guarantee := range GenericGuarantees(4) {
+ collections = append(collections, &entity.CompleteCollection{
+ Guarantee: guarantee,
+ Transactions: GenericTransactions(2),
+ })
+ }
+
+ var events []*flow.Event
+ for _, event := range GenericEvents(4) {
+ events = append(events, &event)
+ }
+
+ data := uploader.BlockData{
+ Block: &flow.Block{
+ Header: GenericHeader,
+ Payload: &flow.Payload{
+ Guarantees: GenericGuarantees(4),
+ Seals: GenericSeals(4),
+ },
+ },
+ Collections: collections,
+ TxResults: GenericResults(4),
+ Events: events,
+ TrieUpdates: GenericTrieUpdates(4),
+ FinalStateCommitment: GenericCommit(0),
+ }
+
+ return &data, nil
+ },
+ }
+
+ return &r
+}
+
+func (r *RecordHolder) Record(blockID flow.Identifier) (*uploader.BlockData, error) {
+ return r.RecordFunc(blockID)
+}
diff --git a/testing/mocks/mocks/record_streamer.go b/testing/mocks/mocks/record_streamer.go
new file mode 100755
index 0000000..11f5e53
--- /dev/null
+++ b/testing/mocks/mocks/record_streamer.go
@@ -0,0 +1,41 @@
+// Copyright 2021 Optakt Labs OÜ
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+package mocks
+
+import (
+ "testing"
+
+ "github.com/onflow/flow-go/engine/execution/computation/computer/uploader"
+)
+
+type RecordStreamer struct {
+ NextFunc func() (*uploader.BlockData, error)
+}
+
+func BaselineRecordStreamer(t *testing.T) *RecordStreamer {
+ t.Helper()
+
+ r := RecordStreamer{
+ NextFunc: func() (*uploader.BlockData, error) {
+ return GenericRecord(), nil
+ },
+ }
+
+ return &r
+}
+
+func (r *RecordStreamer) Next() (*uploader.BlockData, error) {
+ return r.NextFunc()
+}
diff --git a/testing/mocks/mocks/submitter.go b/testing/mocks/mocks/submitter.go
new file mode 100755
index 0000000..14e7df1
--- /dev/null
+++ b/testing/mocks/mocks/submitter.go
@@ -0,0 +1,41 @@
+// Copyright 2021 Optakt Labs OÜ
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+package mocks
+
+import (
+ "testing"
+
+ sdk "github.com/onflow/flow-go-sdk"
+)
+
+type Submitter struct {
+ TransactionFunc func(tx *sdk.Transaction) error
+}
+
+func (s *Submitter) Transaction(tx *sdk.Transaction) error {
+ return s.TransactionFunc(tx)
+}
+
+func BaselineSubmitter(t *testing.T) *Submitter {
+ t.Helper()
+
+ s := Submitter{
+ TransactionFunc: func(tx *sdk.Transaction) error {
+ return nil
+ },
+ }
+
+ return &s
+}
diff --git a/testing/mocks/mocks/validator.go b/testing/mocks/mocks/validator.go
new file mode 100755
index 0000000..cb99139
--- /dev/null
+++ b/testing/mocks/mocks/validator.go
@@ -0,0 +1,67 @@
+// Copyright 2021 Optakt Labs OÜ
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+package mocks
+
+import (
+ "testing"
+
+ "github.com/onflow/flow-go/model/flow"
+
+ "github.com/optakt/flow-rosetta/rosetta/identifier"
+)
+
+type Validator struct {
+ AccountFunc func(rosAccountID identifier.Account) (flow.Address, error)
+ BlockFunc func(rosBlockID identifier.Block) (uint64, flow.Identifier, error)
+ TransactionFunc func(rosTxID identifier.Transaction) (flow.Identifier, error)
+ CurrencyFunc func(rosCurrencies identifier.Currency) (string, uint, error)
+}
+
+func BaselineValidator(t *testing.T) *Validator {
+ t.Helper()
+
+ v := Validator{
+ AccountFunc: func(rosAccountID identifier.Account) (flow.Address, error) {
+ return GenericAddress(0), nil
+ },
+ BlockFunc: func(rosBlockID identifier.Block) (uint64, flow.Identifier, error) {
+ return GenericHeader.Height, GenericHeader.ID(), nil
+ },
+ TransactionFunc: func(rosTxID identifier.Transaction) (flow.Identifier, error) {
+ return GenericTransaction(0).ID(), nil
+ },
+ CurrencyFunc: func(rosCurrency identifier.Currency) (string, uint, error) {
+ return GenericCurrency.Symbol, GenericCurrency.Decimals, nil
+ },
+ }
+
+ return &v
+}
+
+func (v *Validator) Account(rosAccountID identifier.Account) (flow.Address, error) {
+ return v.AccountFunc(rosAccountID)
+}
+
+func (v *Validator) Block(rosBlockID identifier.Block) (uint64, flow.Identifier, error) {
+ return v.BlockFunc(rosBlockID)
+}
+
+func (v *Validator) Transaction(rosTxID identifier.Transaction) (flow.Identifier, error) {
+ return v.TransactionFunc(rosTxID)
+}
+
+func (v *Validator) Currency(rosCurrency identifier.Currency) (string, uint, error) {
+ return v.CurrencyFunc(rosCurrency)
+}
diff --git a/testing/mocks/mocks/vm.go b/testing/mocks/mocks/vm.go
new file mode 100755
index 0000000..6c76c07
--- /dev/null
+++ b/testing/mocks/mocks/vm.go
@@ -0,0 +1,52 @@
+// Copyright 2021 Optakt Labs OÜ
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+package mocks
+
+import (
+ "testing"
+
+ "github.com/onflow/flow-go/fvm"
+ "github.com/onflow/flow-go/fvm/programs"
+ "github.com/onflow/flow-go/fvm/state"
+ "github.com/onflow/flow-go/model/flow"
+)
+
+type VirtualMachine struct {
+ GetAccountFunc func(ctx fvm.Context, address flow.Address, v state.View, programs *programs.Programs) (*flow.Account, error)
+ RunFunc func(ctx fvm.Context, proc fvm.Procedure, v state.View, programs *programs.Programs) error
+}
+
+func BaselineVirtualMachine(t *testing.T) *VirtualMachine {
+ t.Helper()
+
+ vm := VirtualMachine{
+ GetAccountFunc: func(ctx fvm.Context, address flow.Address, v state.View, programs *programs.Programs) (*flow.Account, error) {
+ return &GenericAccount, nil
+ },
+ RunFunc: func(ctx fvm.Context, proc fvm.Procedure, v state.View, programs *programs.Programs) error {
+ return nil
+ },
+ }
+
+ return &vm
+}
+
+func (v *VirtualMachine) GetAccount(ctx fvm.Context, address flow.Address, view state.View, programs *programs.Programs) (*flow.Account, error) {
+ return v.GetAccountFunc(ctx, address, view, programs)
+}
+
+func (v *VirtualMachine) Run(ctx fvm.Context, proc fvm.Procedure, view state.View, programs *programs.Programs) error {
+ return v.RunFunc(ctx, proc, view, programs)
+}
diff --git a/testing/mocks/mocks/wal_reader.go b/testing/mocks/mocks/wal_reader.go
new file mode 100755
index 0000000..827467b
--- /dev/null
+++ b/testing/mocks/mocks/wal_reader.go
@@ -0,0 +1,53 @@
+// Copyright 2021 Optakt Labs OÜ
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+package mocks
+
+import (
+ "testing"
+)
+
+type WALReader struct {
+ NextFunc func() bool
+ ErrFunc func() error
+ RecordFunc func() []byte
+}
+
+func BaselineWALReader(t *testing.T) *WALReader {
+ t.Helper()
+
+ return &WALReader{
+ NextFunc: func() bool {
+ return true
+ },
+ ErrFunc: func() error {
+ return nil
+ },
+ RecordFunc: func() []byte {
+ return GenericBytes
+ },
+ }
+}
+
+func (w *WALReader) Next() bool {
+ return w.NextFunc()
+}
+
+func (w *WALReader) Err() error {
+ return w.ErrFunc()
+}
+
+func (w *WALReader) Record() []byte {
+ return w.RecordFunc()
+}
diff --git a/testing/mocks/mocks/writer.go b/testing/mocks/mocks/writer.go
new file mode 100755
index 0000000..6fdef06
--- /dev/null
+++ b/testing/mocks/mocks/writer.go
@@ -0,0 +1,130 @@
+// Copyright 2021 Optakt Labs OÜ
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+package mocks
+
+import (
+ "testing"
+
+ "github.com/onflow/flow-go/ledger"
+ "github.com/onflow/flow-go/model/flow"
+)
+
+type Writer struct {
+ FirstFunc func(height uint64) error
+ LastFunc func(height uint64) error
+ HeaderFunc func(height uint64, header *flow.Header) error
+ CommitFunc func(height uint64, commit flow.StateCommitment) error
+ PayloadsFunc func(height uint64, paths []ledger.Path, value []*ledger.Payload) error
+ HeightFunc func(blockID flow.Identifier, height uint64) error
+ CollectionsFunc func(height uint64, collections []*flow.LightCollection) error
+ GuaranteesFunc func(height uint64, guarantees []*flow.CollectionGuarantee) error
+ TransactionsFunc func(height uint64, transactions []*flow.TransactionBody) error
+ ResultsFunc func(results []*flow.TransactionResult) error
+ EventsFunc func(height uint64, events []flow.Event) error
+ SealsFunc func(height uint64, seals []*flow.Seal) error
+}
+
+func BaselineWriter(t *testing.T) *Writer {
+ t.Helper()
+
+ w := Writer{
+ FirstFunc: func(height uint64) error {
+ return nil
+ },
+ LastFunc: func(height uint64) error {
+ return nil
+ },
+ HeaderFunc: func(height uint64, header *flow.Header) error {
+ return nil
+ },
+ CommitFunc: func(height uint64, commit flow.StateCommitment) error {
+ return nil
+ },
+ PayloadsFunc: func(height uint64, paths []ledger.Path, value []*ledger.Payload) error {
+ return nil
+ },
+ HeightFunc: func(blockID flow.Identifier, height uint64) error {
+ return nil
+ },
+ CollectionsFunc: func(height uint64, collections []*flow.LightCollection) error {
+ return nil
+ },
+ GuaranteesFunc: func(height uint64, guarantees []*flow.CollectionGuarantee) error {
+ return nil
+ },
+ TransactionsFunc: func(height uint64, transactions []*flow.TransactionBody) error {
+ return nil
+ },
+ ResultsFunc: func(results []*flow.TransactionResult) error {
+ return nil
+ },
+ EventsFunc: func(height uint64, events []flow.Event) error {
+ return nil
+ },
+ SealsFunc: func(height uint64, seals []*flow.Seal) error {
+ return nil
+ },
+ }
+
+ return &w
+}
+
+func (w *Writer) First(height uint64) error {
+ return w.FirstFunc(height)
+}
+
+func (w *Writer) Last(height uint64) error {
+ return w.LastFunc(height)
+}
+
+func (w *Writer) Header(height uint64, header *flow.Header) error {
+ return w.HeaderFunc(height, header)
+}
+
+func (w *Writer) Commit(height uint64, commit flow.StateCommitment) error {
+ return w.CommitFunc(height, commit)
+}
+
+func (w *Writer) Payloads(height uint64, paths []ledger.Path, values []*ledger.Payload) error {
+ return w.PayloadsFunc(height, paths, values)
+}
+
+func (w *Writer) Height(blockID flow.Identifier, height uint64) error {
+ return w.HeightFunc(blockID, height)
+}
+
+func (w *Writer) Collections(height uint64, collections []*flow.LightCollection) error {
+ return w.CollectionsFunc(height, collections)
+}
+
+func (w *Writer) Guarantees(height uint64, guarantees []*flow.CollectionGuarantee) error {
+ return w.GuaranteesFunc(height, guarantees)
+}
+
+func (w *Writer) Transactions(height uint64, transactions []*flow.TransactionBody) error {
+ return w.TransactionsFunc(height, transactions)
+}
+
+func (w *Writer) Results(results []*flow.TransactionResult) error {
+ return w.ResultsFunc(results)
+}
+
+func (w *Writer) Events(height uint64, events []flow.Event) error {
+ return w.EventsFunc(height, events)
+}
+
+func (w *Writer) Seals(height uint64, seals []*flow.Seal) error {
+ return w.SealsFunc(height, seals)
+}
diff --git a/testing/snapshots/rosetta.go b/testing/snapshots/rosetta.go
new file mode 100755
index 0000000..8a67764
--- /dev/null
+++ b/testing/snapshots/rosetta.go
@@ -0,0 +1,19 @@
+// Copyright 2021 Optakt Labs OÜ
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+// +build integration
+
+package snapshots
+
+const Rosetta = "28b52ffd076807814a290c0e029ca10339aa0f0a220a0101121228b52ffd070007814a2909000000682705db1a010020a6793201005002936a250a010212141900001901a94de6f3081a010020b38801a8578af163afa770123df9aba63b431206a7a55d480919a3a4e64f652f9fa3ef1bdeda9ba2ca22e18297a161f1706693a05c2bf7d28f9f731251318b9e80ee9222879d1e20c1e4839c82ce21bdff879b7b05957cfb9a144c4f74f799b0865236f398d7f52328ef97d2b89899eb9680992f8ae795365739b1e394ac6d8f70df93a1a7ca6cdb926ef2c637d791d3cb78bfd390462ee93bcf8f09c689a5cb8e10571ef9c78df82279e5c38c335fa579bf8b805c5d0cbb8a579f6237b7897fee9f65b3880a6b95f4af877586e8e2ab86e0b7a813a7850f104424a384009021799f83549216999b827d2e0bc19781c17d372c9380025f4c118f7f3602d02c8b7e26054687877d6e7e7d38837ca2e14ddbff867be00c2d23fb7a6153bcdef779d9be640af378c40b6a92ef77c223105feb763705aff9e7755d356d05e37409e25c60df7344fce1f2db7248d9d524d771870a251bd3702f7505a2cf6ff30b5590cb6ec6ed1312c76d55941b01c36c834f5411bf6b0c8583babb6a56c227afb769e83aba26b3681c3de296af67905d5074ab6675353e37a765a22039efa3642a6d96649f6304fd01b39b6295a148f49761e8e246a8936052468ce88f5f4ded1cc48b5e4c7c5d46875d063edd6a835c32c3fd79ff855bfdfa4a2bfb5ae0d74b17f7596c86f3b0f3580957d1a8ef575d2f0f6feb563f9bc3cae75550022284e3542f6e7a40df53e95463badb52a77966f8d751c0b0d865d350dedf8cc1cf4f98ac6ae7cb4e5b90af07c74da4b84183c34cf8ce0889bf4bcfbe368cbb4a9ce36a30b74964b74f0cb3484c687ab3af472903eb3bab46a08db792a745f33f8746a344cb5d22ea9f43044324819b42f1daecfc9741eb149c019340fd749dfc8f3f074ca6d78b3ed13f18a5873d70ac2f0a833cfe851365ff843b52697fbafb3a524e5195f739adb86789f3386a606da6ef37c11a9f78eb366f352784e735046ad63fe334a8c8ac05df330eafb87edb323475f8c9d73149db6f6bd3304d2051e0cf2fccd03d97cb2e9815bdebc72d948792f8c32c3016a53dbf2be0a6650bbb2a23785ad4b7297d5a5e49b32851702ed2af27af317bf5ab261cccf3f0a725fc437628a324270f59cd9f234f2525b29b22466283a89721d1577edf932053dd46178f1f30175b758b1e75ae424a871d5e5d7175831c27960059ff831baddf5a8efb1a3452ffb2f7190630b643f318f4c7b739ef17d4c51288eb161964f59ee7159f97ea87e314085f4ee3df137d38ab20db127cf8535dd71147250088d3101f97392fcf0faee870c7cb0e59f21dbdc70dbaa361ddc30ce8fb61ccbf0b68f407a8bb0a7506a973b709265a8ea1b30885ce8d2aaf071b7c7a09ab069ed49c27a7054e61d96ca304a327e2499f033eada15a9b0290c9c0479701055a2d3693002eda2bf88f241311000018ff1764dc551a0100208b241311000018fe2d689cd31a01002087fd5f53f15d83fc87a1e88bff82fb53d16084fbfabf8be7fbf7f981efeb4cf3f8a0ead778eff76854d097ebf6a3b4cbf2e7f5429ac86ce3f4551aa552dff3875c0831dbf23705af81d7f15a4b307ed3f0b162cf82241311000018ef071893511a010020cbee5e6a93ecc7ed59c201bbc3eccdd66f6ebfeb7ef3c7abbbea838136b7e911b26863b3e858bea9e4afe7da2747c8abe640f81a32a7e5669ff68aa3e4612a2bc59fe3a9f4680d9be2b65db68097e181fd50bc93e03e3916338fdfde8616008bde0560fc9587ddd61371e983dc31415100ff81dbabe83821fbdace491f4df7d94cbf6040f3d88f965135efd7260bac38ebd6351a2451e7d5dd665d5be3d4a47f8d0edfd3a0bbb06ddbd234d4605cd7d15bb202d6d3d0cca6e1d1cfcfefa32120cbce8b7c0c48c7cdc853826ec3cc94d9b37dbfcb1605008abbca57eeae9cb7c9b25d79b7b3c8e144e840afc7291e081eabc6d00f3d5da7c54f2c2acfa3c4ddfafca69fc3dbc4c4389bc25438e4a597c1b8ffb3da93c0fb1676c58fbf78bc04ad8bbe4a1b4b1b87bd572fdbea83bc574c0b9bff80bbb956c92bfbba67e31559f7b9a4f480f6f3b89b15088cefb7f3fda8cdebb6b62ba632e7b592fe8827e3b4ef4492ffdcb3bac550e4d5b20dd78db8ceb163e2752dc6b0c3f59426c2af92a41e87bbaed57a979fb7ad18280c53b0acd9c15e9aa9ab9f55ebfca2aa8637a14a9ba9ee8b197094a84f42b7e78da709cfd05c89a6ec3887d28223a567454fe8fb7f23a47d2c3985f3a350ab87d1eca23de6678de8a1b4191278e1a01b33ebaeda9faf66805ad39e9cd068dccc9dfe7e4b39c89c1ba6d9e5c19bb0a49677ba9a60f9fba7b399c22a178bac98eaf3f2daa897f89fafc6a196aad451ab9a959b26e75793941bb5b1818f93f3f6478288924a9f79818491b0d72815fd7e90cb5dd09af68fbc0e7ac2ef8e8e948b57e88d877b7969e18ce9fea5edda8baee33723d38a97537e8ccf89f7639775cb88555865adc487b1c51504c086a5e701dfb9850c8e7924b284d4b8f46cab83ef58d018a78253f18d43a0817395b5949980d59efd40957fcba1f3958d7e90205f21867d9824efc0827c8d6a01a3fb7d7bd209dc87f77a3ddc884bf0795f57f9dde978f60437d3e577963e0268de76b5b0dcd8da75f6a8d019d674ea6438e5cf73ae1889ebc87213204531c1719d8659dabd70e793444fb66feb2c2292b26eb7d050b4ab6d366c7467a76c0481b7b7a06b8f926062996a3035140495690a3e2bb08e68a8375fe18a6758c8ae6b836639131ae5ff7c65afbfcd18f864830dadfbf463459b1d7fed62da537bfbe961c5038955e560f35baab0de5fafb5da5e9b13964cd35d86e7a2dfcf5cf8d11c58c85bf61c98d4c45a6a099d7cbd59bfbf1d8fb958c596fac9b557550a5aa5ae5612ca8e23aa55e7891af2a6549e8357249f535ec29cbc9b5268dc5e52975118efcceb9050665c44db8c4fccb617a9854e34fc2f8f814d1630c8cdfa7b4cddbbbd1ff67b4b6b6fe531f27b4a0d068780ee7b49144dd888ea7b480de37330e67b47c82f7c31e27b46c59e43a5db45602ca9a6d744d1ce7e7cd343ff425359cf42d9bea7acc841238c4e34c0403b04d6a0bc3f0cebd411b83e71ee41d2b43d6fcb62afb03ccb5afdb4ac3b28767c74a83a2086bbb1a439b1c917359d38b1caff5c9937bca5a37c95363c7db2ae913597f73d348a348f61f25d863339ea5b2b823244e1dfacfe7a310f77e483f77a308823f45bf37a2f88fba2b7ef7a2e2a8d469deb7a2dacf888abe77a2cf17d1ee3e37a2ba0bd3253dc2ac50bd59dd82948f308b0d428e6a12a51d027d3da41aecc26026a2b29c8253d83342bc124496cbf24bd23327edf05b9220f387ee3b52149323542b120f1329830ad1f1eae0ccca91e767aee13a51d9cfb6025a11ca87a43639d1b513051b3991ae3fee7fc9519da8268ca8e18ba7e135e8a221209000017865cfc867a221209000016822b2d8215509ff4bdfe7914b7f4061cfa13980a9f3bf6123dd9d349f211f74fac80ee1079f04abdea0f3c25f0e4e60e60d1244de20d8b73ad38de0cd8b769d3d70bb64c9c63d30a2b57e4cecf09561d1d16cb08a702700ac707b7bb58e8c306a57bab27bf050f974013bb0432679dd6b703b64b1fbcb302e7b03257af0130e7211baba724a0de010a090300000012c501a50500140aaa645669657700664865696768740067436861696e49446c666c6f772d746573746e657468506172656e7449445820006954696d657374616d70781c323032312d30352d31385431313a32373a31332e323433303836345a6a50726f706f7365726b5061796c6f61644861736858207b3b313bd83e01d13c449d4dd4bac04137f5090bcb305ddd750298bd16e5339b6b536967f66e566f746572494473536967f605003387fb7ee87b4194880572038ddb03d0ee142e1a010020a3906aa60401128d04472701d50f00641e0201d47b1bf7f37e192cf83d2bee3f6332b0d9b15c0aa7660d1e5322ea964667b333383a34494458200555337e0f661ec9b07ebb6946081316fa65c0baca6bd4705bf69d56a9f5b8a358608f7a5cbb4dfa4606e99ad6eaa1a31a3bb4a4c0a44aa6e7e68b5e5e8b3f0fa33268ee592097a238d52564c3541f1f9c5aaac501f3ff3f83049fedc384f805e6150f2150273a72e3a035ec4348409beffa1b20b60753cf389f87f05247fcc470478358204551be34d9f7a93b00d287bd683f7dd6345e65907240400554fbdfa1697d3bfa5820d35faca67abc06c334b1e5a7882398dae9c1dad913e5609ee1d463d55a2244f7a02aeda7c4e1408e70e7a67d819924f47c304202e5aafa8989da9d22b545b0c2a44c4bf3e1df3173a23e4805b4ec5dcf8a6f42e7ddad7d4b7e0ccb0c066410864dd6893a6f1ec4ef6c18bfd53a3625f0f9b01f274e4d7234f451cc7d8186edb205006a1380c883335173d2fcd811a71e11a4586f3379966aa702128edd03029eac11ab78ebb9650803eea70a48399f772c64892823a051298d445459cdbc4633309724c4fdb62bd7874004f749aa22c64a9ad8e1d0efcb28f5e69b69971130e9b7baf41896ff8a4a16ddbaf9a0118e7cf18fe08a241c530abf07d81e8eb7cfffcf35b3fadf19e6f5b869b65cbfa8ed7ce5d91427151eddb2580db5e3d7881145c290a8c144c7f7f7af18d505a484cbf6908e83672bb1cc6f9ca3aa088e2fe32c21da8b79e6d0e9512cca44733ba8e8f89aa10c02c627b069dd1396b8970f9e5132db9c48d867702d29851c2db760e8625c33169fc2912882954ed6e87626aec1f2e0dee6e68af9b1234e3d224801eca102ada79a6aa6030403c14c27a0cc21f39d9fc5e661d5bc2b267eb16ffb0adc40cb56945d9ca84eb31333318e5e01537f62be56861769ef5bc170229771f969c6d00e143fcc3a2b6729f515a516bd2fd80e9ff1e99e84091ee35ab6901b324c164fbe7e9b04533cc14360b466d6ba7dfa5084450e68860fd1529d1d30b4a8b47d7c5418f66d16176dbf177ea9bd69f884f11957d2329ba6958ad1aabb8def4300157abd7f608e85308287803288a0c3d7b5773e4c9b7a186458942f991ee6ecbcde7f44dda5cd4ce7e31d32308e8965a7305201f2ff04b6fe0b5949ec4efaab3afc59465d4c31d924e8b66e200f0e9a8a75c6dffab44cb10304039e6aa7040504d8d5b941627ae2d3cef38bb388d3e59bc9ef0c50a1f676f8ad1ec2e7cf895d6a33e1da6f0d2534f369c6c8898725d2568b479f26c81ce1d74328653d5e3205336ca765e3dcf733e5ec61dff334e711d1fd6492d269eb8903edb59eceb83414c118621940391c107eeb01285ef4a3fcd6ebadc04580c57020bcd1438096c7cc03c0dac43e2fb6d794f0e5b9fb8cc4dd6b56dc54eef803326e3314f3a9889b6058a498532da83faad858436f3f004387bd5056ec38e778649e15f4a0ee7e561640a2b170f869b80dc9a0f118c7408080ec2c8d2841ea68353298f6358fc0c93c63500144ea908f12cef1ae485b0ff84f39eab33c3b9b3e55aead66e98aa6effc82e94956df95b5040504a26aa60506056a435aaaf30faef9a87a7bbd0b687a97d7e2aaa48978c8f54f026d80074f5fcc340ce8f03b711bb29b88d0b4bec916deb7a99fb4d10f8035bce489ff8e3e79dcb38aedf619a35e0499502f071338407f0c43de978c9ccd32ccdcd9b2f030993b53f19ed6781e3eafc587d6832ed490ab759851212ac940885511ba33f165ad2e8e983139642288b7c39d98e3b9964b7e767502ecb67be94d0739697c174b5e9e6124edac9ad333c37de6baea3be2dd7dc1999c0de20ddda49bb8a193316f3a225df8a285eec338a9944e8450b4418a8f838dad230c140358337bfc239d8dd5065492b27f1515b3278eea9417300283616ee47a13bf985783f09b44e158d3345305f39d1cb9050605a66a0607067dcba1fc32e5b6dac03d7cfcaa4e9d47f1e6a69e2af5bb06afb60e184d66b584362d7415281ff531ccde8ac9e5bdec97ec2190db707949d6c54c70ee1d3a53c025983590c51bb53515251ab1520fe5f768a4b7c601da59a1a2231f18c8ffe6d110576635b7543ed46c42e3bc7fb6545949a1f2ea303bfd0c77eeb635f2eb26b9d4060b1d0cbdc0d01f91a1fb644f8bf647bef3e77fc54aa1a29ea5c02c9d6bced08e773b9cba2cb5478f7ca974325803948bd9a525b062ec1672f707e0f692325f630843e6beaae163f7575cfb218a5239b14651d0ded82948241963b3b788e5a156f50e3fbae8673fe43b481d19e898615bf82c85a82d101ab56083b67272b0cb7e45d9bd060706aa6a070807885eef0a6986692f3acff3b7e5d57c61b9eccfddcc75a2722b8dc0ca97c4cd90370df864da18d23a442fe0780de30b636c8e99fef178af27248344480ff7546faba359bfd94d15553c9e6ff74df4a89cb917f1791eba7f51024bc3fbed314f5b3f4d699824e30806748198ab5f3838386698217c48ee44ff2df9c47780e0d387ee3d107b3b0bf253f2fb5222e2a3f00c0df98e482e089ec3f674b3bee7ec28157598cdeccd24d3a8be9e1a309c1d03eaff3c3f90b725de13e11f16d2709835e0cf966d910c4ba6bee352bbfb75096618a6b65aeaceae59125508a024f889196f619fe65cd3db85ea03f08b344201d878dbe41ac8641c170052b90f6e3a7032b4fd4facf0dac1070807ae6aa508128ccd0f00540908462aaf6e5608babe0863a657da1eccade5a7e2fb8f0702fa96bb36c2de5eed64397fdc6a8b67afe92e248077f676161c8b35f40f22f9a6584ac4487257bd86632f8b2ca403b14990aab592501baa698da92e71eb3ca422d8cb39fb353a347b98e847604bea32aebec793072c7271934b4ba915e0e3f7c278fefd0a32d4fa9f6581fc4958cf0a248c0c06c211ca04fc5b55b1c98a32840b65f766439ec747487acd19c4b2d5d409f5c8d67896f3252b4eb8077ee0e440d3a55dfe5e8b4a117b0cabea38b7402f6f578ef7fec32ad23959b55bcaea973f9d266fefa10e733bd8b94ffdebb4dd7c64b443025a848310645dfeedfbc2df9b6a86f0951fd01867b85005006a1b344acb61c5a5b26aa6090a093de7a2232681cf1556a0374c48d32249548b7ef347f404d5252a652f23c23a5634302fe1c5df182714396bb061c4cca6a3a893315fdb045dc11ca7e40462db62f5c1803b042b60848dfa073c337fcbb07a3e157b19d982e57bafa7b597b8eb23fc5652454d0c11093167b136124b2425273ea68d33aa8c4e064e40d57223289f95e1714cbeca254e66a1438da896b5b689633ca007a86c341e28529ab906f4bc6e968f3572027a6cf7ae6eb29f1a6703c7b3d7222340500e03e3ac3935899b5a3d1abbd5946a20628e23cf45021362a43ce78df0bef8d3eafc1133d3fe7da348efb45a6ee029e529bfc1634396513887845c5e0a8622655ccff9e36c77772d3e7784bf0e5db4c9090a09b66aa70a0b0ab0e9b4314ac2023e1894a06b43433523b8db8af1843e9797cc9baf47219149763432f6170ac962955c02a48846d06e2ac1e1e1f095f2606ddf88329efe5027d108e0b6337114f5194d56373aea38979892dfd7d01a5ca655169ab1170f50d5b5c82a8b5b325d6e4cca3def7de0c909298c248343836cad16ecf3ff78f0e7d5a34a314bdefc2ed3bdddaf44824a50cff61c852da829626c280ffd526effde9a29aa72a6ed88d4db3937692e37a46155dd824cf19c9b7811642760f3526b4824da27d91e497aeaa700150e7afb83e7f41bd53b993d819b869fc3d056cd0fba9a2689b50dd89d906a3c716c9aa48a646b4a26a9f89e7015b4f93a6e5c691f40412a169b111ec54ccd0a0b0aba6aa60b0c0bf79deaf51438b60b9971a32644546c688830a649910d3d5beea7bb1b46fe436d334cd9c6895cf3a190d5891ed1b1ca8242cbdd061a8b259119795bcced9851456bb14639a419adffbf729aeb77a2b3ed78d8e45a033386d6de82ce4e6712ac6a51f830f9e26fc9df804682638c765f9cdcb93ec235a85c0601b85eea9e698f72a12af62a404f6e1f9a2a95f4ed7ca82fddf35f81a87559c3ee47ef42136103c13d92fe793e64d6a4febb08324eff60c07412288f15e07cf98669b6765287137ea8bfbe13dd01902dc9f63705a8c5804b7292704bbb8a752d9ab49625e9e307fdb5d97e5a2dd831fa94a57428a3287f7400f4bd10ca4ec0471da37ffbf601659a828eea975ed1a6be6aa70c0d0c46d12fb8a054f12ecf34eb34e549776b0e5a06ec167d3a161cc42df4917c891c35bc1670d7aee632a79d27d96e05b9fc5a11bd3593b397dc08d5bb123278d9c52293af8c0f7ea48c41dab4f885cf1e07daed046e97df23da59c66bd861d03d9922a61860a80ebd4dd25e4528ab621837de86e23f2233628617e38b38bffee2c28a314418800ea645aa6a113b040158a841c07268a21fd036baae112b83100f126cb4a77cb0b058e95ef4055a97023265ec0113a2884633dd5367a335294346bdc97b009bc8f0debace8be793ef58576d3797bbed63666aa35dba861aa2cf97edb7487d9d5bfe3ca4039f45e8dc998c7e8c61fd443ffa17598b594272e2d3ac4755077e71dfd50c0d0cc26aa60d0e0d9035c558379b208eba11130c928537fe50ad93cdee314980fccb695aa31df7fc36d04396d90579eae8fcf5906fcede4d26be667d5d6e368cd199ed8a66178467f298e2f946c4d771c6f656210ee7a49caa0c3f7a75b9539537ddb74b7ffc1e1ae9febb562eb86ef6d8254d5fee461d0dd482eb07dea2085813bafbeb2ecd882e7c1be8c51d840ea210bee3b626874b6cbfc2e085f47ef5f3555dd349ffc8e3b55392f68955760b05e589ae3e21a64a4fb6d440cc94908f40ebcdfd3045d794c895fef17ed871ce6c03b84f5f0830028a85902ac50d814911d937356ff93f7b5204db5a3681daa647b5d9a7ec06da3470df084ad5d014f72dd75b6639643cf1bbe4f0b11410d90d0e0dc66a0e0f0eaf528bb047d6cd1400a326bb127d689607a096f5ccd81d8903dfebbac26afb2338c158b48d13a964fb2d3b6e149622feecce76b174a5185327a663634f79c99592a322306bc6eaff0eefba90486a818f231c443b74fd5469f0c4387d604c5230fc7bba071d3dc9d0d7e31577e87a297ea781aeb267b39a44e4a904d32d60e88abf6e2abc229ed2a91aa0876bf9a3422a45ab0939106df386d758c3772e32b3c43fadc9c78f7927ff17d6c4669b7d6303321696ff23be284a3167f83a33ae3d9ad4c7bcd4a7dbe026bc650c1a8b940e027394e5e9f51c5dd057fe9986cf31eb63f2f0e94a6a9cad279bbec04c4b62da1089833a09f9e665fe4e522705f48ce01f01003650a3e00e0f0ecd6a0f100f26b9d551b9e04a9f1b83686c99d2250e64c33274fca09c8f084cbc07c99e6b25395eb305cb47cb3dff043a3ac2e322d2ca7eb4c1972f36ea3e64863e8da0fc54958b70608f884d56d5b8f98c3d47733dc73db9b6b15360be0b02e5e1044882f41968ff0fa4c85a8ff29cca1f2a9aedb04ea5170a4c656ba6b62b5bd1ff136b89e9cd7fbea0c01bc14ac90b3b31e49a23deb967821dd13e561cafb0c2d9dd677d67aeebb274059f6098f61b3d0ba5a246fa2594124cb50e0391031ae518ed5816dd4fa88123e394d5f63968b389195fe4b89683d3c573ec9a9af5c1b5fd2fa643ea599cc751e3a2d3ec422665c83c1636a9e18d34d3a61f6a19a30d1c823a435347472523b5e40f100fd16a1011108b4144109e8dd53bcd9020cdcf8aa07d0857782428e8ce8983caa83036c986ce3531824919f6244f3062c6bf850615843d4f13df13d5ab1a551af8393a1dfbc2deb391737e0b10e1c92443d4f5671ecb4cee6ddae11f1470b62f50f9f527d07235c9fb1a38a0aecd7ef86b15cf9ecbf36d63af0670fa623caee08199a4149cefa83b1d714eb0511c22e54f0f6c0de3581cd933a11a3d8cc1e2ef7c36158d258a337886fb63ebd7ffb712d6abdaa75e6b39a2d8261a0b2e9998d0ba8591118f8676e43e24e9f41089d8042857a4f409dfdb1d905aac0e931a5df759b704e477907b3f836875ef863f5903764b2229c361e592281c81d5dfcfde031396a571abc8c2a198a901dae8101110d56a1112114e7809a37e2e5d92f535c164054e9f4b62845ba35065622aecc6bc9f22ed870532bc263aca95264f76f8447162dfe7fd03b753211e70cbfeec998b1771d5e3517eb56a0c395f6c8dc65e03fdd0a63a48c570a5234715a93b115f42b69fe56e7c89b8b9d01bdb4fd53f384e2aac4cf9b0e6996605201587fd7ee45b793a2edda5827f91ad70333cc74218e5c4115b828fe770860798edb5f779333d1d93810229068d6e4e842bf3c0a43fda3c08e02c6b0660822e6f0432f2b5e4ec3d38d101d2cbfce467a7c7a17a0c142fc10ca63577b1ae63475753b6f00f06cc5f97f5df886f3d07e94c91ee752af64a40edbaabaf45a1d1e681c182377ec037adb3ff99569d242fec06ec111211d96a121312d3e38b482a54d92f83a455bc51fa5a97d27fd10ca8f0e02aa1f8dec0d7cbebf234d244b2775c87f5187978c7f63aa23653ccacd7251684d3af63313daa545a49808a54a165ec1ae25ade2212fee2c1af9f4b44a1b214c4fd16fbbd150d50400151fd107ff2501eda88f67119b6fbc9b962b7ffac29c877c0d7152f2f3113d1ea5e7bd46dae39da020f197da645a1671b6fac30263eb847b9c4b8faa6e90e2a6450acbd075ea82b2a1ae3b425507b736377c657d4c9e2c4c47c4d90027866fe39c250dcd0479fa0379f6c85c0f64e2647cd9390f511f9649e6a64aa7d8e5dda8a3140c262d34964490d16a89b74652e366541cd4c411d164353e9a72d5659f57e6468a49d4bf0121312dd6a131413964d906f0faa2e36fecef73dd3d928a7d4d6b44e4692f641729f26dc4e50bbc335576cb2b40ddca851e31d770f5e180582ec7d55bf1863143fb76a2bd9936a9a0c8e4245e4daaecaca2626e45e06587800d99deaf4d82f726b02f7411dfd09de9c88c3cf35ce1cf7a89306eeb964e881c8b5cc265c1fd3cc07d9fca3b129bd82f60eafe177df5a8ecf5388c8ceb643ffe81aa2a0eaaaed23a3bd2dcf46ee7e2910869a3995083d4e841ef13b3a848e570dc871597a67ed10bd698db75088f4e656e8480345e341e6c43ff9682f10097e23a5a204a1d74b6049eb6b7fbd78763d5abeb548845f8c17b02ca80f6021d5a60d9e523faf7b9f1ee69a873bf53b563b63da430d35f4131413e16a141514cf169dd63363bb4233f1c0072e70cdc5232cc6fc79dce4419bbac6b3af9b721c1331ebb9989ff28b4a9203fe8c43835061bb56e598519dbe16614c4d92a683f2ab553ffca4e4ea37a15e9000f1e0137c9131c24e494bd3619a7282203ade3524c069584fe651d7d9e9eb43258df6e4dfa3031d33e78189fd105f998495cab12491e7909a56ad1ea908266b6bab4569f0575b2459b4917bf625423b4aeca73a939295ad8f64f574f4154d1792a608c19af0623f1ea6ef35ca3565f338b07334e13d8ad6a2882695290fd3bc800d58a1b8964c613fcda71eee5972adfde3b039e43d5dd9aff74115543c3c56facd9588a6638a1677f80c11fd083dfb8a0e7b21d943b6cccaf879e56aa71516157b13a2d59f0f5ce8e9596385dbeae195cfbf7b2a6dd23856b39c97aa616c68723509f9171850259cbb37b82e60c0e9c0afe0b2d8cd30107312bd0752838d55a9a6a251a6eeb7363c846054878bcf1be605cb791f448222981fe7ab797bc2543ec4f1f7d4fcb2dfcfb3753978ec176ec7a7a2af206831d31f68d47e8d59804580fec382d529b23af47b5861552e67ea67ec658b45cc966f4ad2740fae9537c100eca09109206028e73ab5cb0ade53bde8b51185a17ad3f143d9824b8c095b40cc6f03a2a091c55e294f0031ab406db9235fb86bbb024fc2b7a940b67a3aa1405f92936ccd33acb3f65f2d3940899227d4977b10d4b894a49d29776359cbb92e805119577d11fc151615e96aa61617162de272e4ff48ba9f4ef0fc078984db7db717575b69ed3f57734f985a09e188cb323a30ca713804f2217ea2c601259a932147366a81daf7440080c9eedc0fe3a504b2c7968e1039c4f44186cc2ac93cd686ab9cbae64811db11522fc3dd6c52644ac7176ea21f548242b789c32ea8852d7c3b2281ac15318b03f03380b0ec030a934bcb67a3e89a307163d1c2974f2e0880ada30bfcf1fba091cc89c013c6fac04185a2b52233201f3bb20a87483a6d20a1fbcecae5189c954ff1c3147f933b5ce09be0a34f0db9a8c7892ae24072c8efacefbab7182f14e8dd2a6a089fbd4bff10cb3800941c90cc796fbcd938812441219efea655087e30b594af2549924573ce48a2d112f32a807a161716ed6aa7172874181817525c36fea6662e4feae932c6a84667ac3e9093f2616e1096a1dfbf1a3a0a7fcf323a3031fcd0d4d4f3b5825231c242c52fb4546b3fb3689ee2d1612ac9921b16d2ca582d8e51ce5368b0828a15ac2f71466138cd0d31b3ce4113756fa70a4bb068b565c83cbb188083ed71db8a1e4fda194362fe81a795f817ffb86ee5f1d7fecdecd16ba0ff55218021d9642e8d1b8800172c12799b6487ff716256d8ca8f063814d6d0b4322ad3a1737078bd38a084083302376565655513df0f1bfed5f4dd604cc307e770a7a426a4860917091eaf28d22722ad36d4a7893804b5833300d85b3474a2086760d47824ee36271969d2847c09533ff1020bbc577f086d64394ea53b1ec2a91e11a42493e60284a7f16aa818128f2901e50f0084191818ba629862f3600411f9f63f532517b35a3a79e0ed80d1276ffce14abc2b8276323a304865e192328235aba6e990076a0025bcf16d63640007faea989cd30cf8694c4da87b970b543c88e971fa00a9f28e7ea5f76367fea1603091b7ee63815f9ad6355c81f263b280412051c2ddaa60ef4e62a9a6517c655c3b84c0605f11cb89b7ad53850b3ca4e2970516fb20b221725469805fe95c1b3e6b8fc3b352408c0ea9c0ac9ce8078b8b2332b8df8b7c36c808164129e3873b8196b78aff62b5c782f2387cad4a031fba656186a1ec6d1e88f5a480be2312209e91efe3206060f626df4edd0ea6d862719b2fb1a47f93099a1977b423a0452c6a8901b56dc6a4134b9bab1e11a4dabccbe2887aa8f56a191a1969e53d6a715a6703b4cbcc07002567da45a7872fc9b478b7da0063cc2ac9080cbbd9bfd322cc03d631223bf1e74862a86009807fdec4a87d4653bfeea1d8ee62b734aa4e608294480da02ebae77ba2b3b538e7d3fac2a096a7b01383d338f24ff95c23dc91a0ce77e2cc1dc74ba76db88e748f2f94414d2b66093b0d779cbb4192e8a74860ca4073a5cfaa8daea293f8cc2ce1a4902649ede60edbfa0b260184ac791f26fb8ccdcf759eb58e001d456a1102b03a7e6547ed661b6640d7ad9907a49a3f4eab2a0aec3d0136f3286dd6c0b1f72bc7e1fae6839f8222e0ac9119374d420a57be68a4f59fa764060645c6d72ccfa635cc57150f7efa09d827b9e742ab1e11a4252785a48c7aa8f96a1a1b1a2881c83e5f2048e6e161f5ddcae006202a9cd5fd1b303bbb96ed47d222de5ad714a8c659a8e4e1ae28785d645b969ab07fb4e72911a557f4bc96a6a4f556b396a57d128a5e52b329897b530321c1a3713c2f4f163f72ec71e7df447a52c3bc7cc8c48640453762ebd7dc35650b4b370a965470961f919aa8dc191165556f3b73b4d4dab566e773088aac2a7fa8efb628921886f64fee7495e0b30a94841345a4b93d064a805f8e6c5928838e943ebb8ee75c6e8bf3e07c9cc8b4e4e4e12384e3c52fe512ad3596f7cd19a2cb7b0c0a8080e0c82a519ea4cb96965cecdef4013672ffe64116e9f531f084a5e2ef23712447895ad9193d7fe6841ac786ac42fba40ff0416f901a1b1afd6a1b1c1b57287453ba50c26168630bd364ee1d6d5a62cb47e2909d8f41f17c4e4aa401d99257d571c78c9944d7c19d5a7d6bf3a186d55e7bd7fd29c19fd2d072e233c8c7e508074f1206ad7ab5e41713071ac6baed728b3acbf881489f93f935c672dead5f32cb2ea1ef5d3727f066b451799590d94afb72e890b0ddd89866adc7f5968cc0bda6a4f0d60717d47d902dac53a673be85283f016df84d60f3d9bfb94346b6b74d412fa5f0785089831a3fb74c030b6529bf5113e0931f3120d7cffa1d69f98452c82f214d1606026a0a368ba82198409ca4dd41f40ebc399c2d6eaf6114853c94e06c335518e87ee755b6d5b3f8d10cef2324b6be5f8241dd71af3a37fbab1e11a4b082aef897a8846ba91c1290ed1d1cb539473d2bc87092e421043ba92df2c959a68de5b350fe74c83f5dfe8b9385f361f84cc1d17939ed531a2388096ddaf9ade62567beca05baf4ff0219a016d3c79832b707d2385c923fe6bf99ba34ec7bac0a4415fa7ef5ded23642379e07d72b7d648df874e96c9c029f39cddbe65c2fb94a5dd1a8ed97fc32c8146a9abeb6cfaf878be939d5f5bf90b7085282ada9bbd844566f43ab88e89502b7c7248e17d1954d2e66bf5fa426e2c1667a402213fca0fb8f15598c443334bb144c9d047604a678938db302dc46b4fa77b102789500affb91891180ce6a4509dbdc8d677134c00f1adc1a09cffdc13af45991d499f4a248e775f129e9b1247e9a7de7536d7f56880f234c9b7aa9886ba81d1e1d34d91642f3a2beafd086974c33f6c120170d9db78791e5c87eb2e6e40d4b2dc931fdfa8b54c1d685f9264204ad0c5023083cfe6c25d41181a12e95cd777b6e072c80241f108d21cc75a5a31f8948f62a5202cad313f81f237aa6dc882f2b61fd42f3c738fffac330d491ad58b6b4a14cf7b8acdd63cb5ef1f753b2a922f49dd0d3a6c6ef0296fe400e27917f2c01a8c8731e81a10d54f5687ab21debcd8d6c9aa285e873de97cc9b8498f3128f8280533307456b1733bd83b114745d48e6049b3cb43ea61740d7d34b75faa2f12fecc818a40ce9b3a461afebced84e2621a4f3429354258426df63d30f686278dfe2bb27876a4109ffd714c77af8629abf9e5451ab1e11a417e201919f7aa88c6b1e1f1e0a60f4a4cd09a1a648cec0bc53163c41baec35d22305f06351816bde67509e37dd3225678b74be375d044c5d4e80623d69c5d856e870d2e3f0e013eda1d1f5cd994b25b2e83c95feb44558582226c707b07ca0b119e08257f5a08163fbf61cdc8b03ce08d1e9a9bd8fc4507b954f811ca5b0b59c9192ea43ad498288e12d8a5c2131281637b34b0fa1c08806a0eea8af613e22cb9817dc83de0b9e08da8bef438201df2cda9ce32707ad6c2e29522833f6d9a5a74de401815e9f6fdcda06fef0cd6e23535b21831812abaca13cbac46e95e92004a55efa90aecbfd6c3e39c8ebebf434615297039a2e081d359e6958038bde921a762b0dbe2e38303707c13ef62c9d6a72a31e1f1e906b1f201f10d16d0016e4b73c70ad95cf43558fa6b0ce1b6bf41f50e27b37d5452a4a6b6e06900c10364f41f1de312c269b0a9c13b0dbbd0422b4d3d4dbb0f93266738ba78b606d562c6b968e91ded6549dfee7f066d407339fb8b74803c0a53702606428259706f56be33dc542a859d5a326354894bc8edd3e8b959e2f82a8c95ce2da2ac8d144318686087aff2d2b9bcb2c3888413a57d848235ede8614039d0ee603cc8a40cb9900902977321fc1d689caaf44ef6df42d05ad46a1bc6b6ca426b5b7a3179cc204c4d4667c553f90378df6285db4e593347c34e61c197161daee801fb2675d4d9dc76118621ab2b522210bd93cfefaa57206ebd81eb2b0ec1c5df3128c3b67284da71f201f946b20212032f0ec7b34522acc337e29e0347fc73d6fd87613ccf8066d5d3820447f49925e496444385bf5cec452f3528a154601f773b0d824c6ad5ddd61b2d2cff650b5b7af0e0b33e41bbd8e3ea609808f0812fc874ff1e2957e2aadc222e0b3a555a61504688b11a2cf9b9dc99ff3087b9bcb45b93b87842f753bf7a2c4144e44ae4019b6b2a1777dcc987e4f073caa6254ec7a11789479a9961b165e656a081c3d13e8a63c9389d143e638d113806f52af77a5fcba03ebcf0c33f9fd3279921dbd79d9e8ce2a74b08ec6889a2101aa364f70ad90e6cbb81db160d29a6d102d86a652caacf0c335e2b8b51f1a7b62e9a68e9a8fe896d8919de7e74dadb989779f5c21bdfd0abf9eab202120986b212221a849f0fbfbf7c242a92933ec3c65731fb952f13d65de39553b5a299b99284b1e480ed200efb1a468e4270dadbd4cf78664b629ac88b08c902cc254799c2a5a0aa417ba77dd59620d86008add6c9eba823e1c09088aaf0b0f2a62ab0250e530e3ad30ae8b18a3739964bfe64432afca458256e81efe3a06ab9bb92287e8285c212dd3b7120a08633d688e4114600c1e205247733a96c24d515e1ec93184fcc7d58be8c4f1ffe87d6e1bd74fb14d4043681638c83ebcb29f71a678fbd5ee6932c9ed71884cdb0d1f19a26b5bdfe4854877ad821ac834e5141172acdec5e97c72ba4e86af26f578540c1f6760085f3555d9405ac17195eff2696f64daf775cc3a17b0e7720eaf2122219c6b22232235f00f02ec8927f272f2fd4386f4f9096d3e343018ce268c47725151427c3822be48b81a315ae40a9893d8181eef6f005dc358995174cb028de93cc90cb0b05587a0df74c0101977e9c82cf5fcb124b5ae079b9101ee2c88bfc80994b817391118ee99dafab719daaf49dab9ebfde77ea1be33d4d5520d5a021b8bc2de089f51183ee085c959a8bd1f6ceacaa62a08a1335e7e8f31011bb2952d219a34cb9ebf95b94b16219105fa794ddd35cb2d945912e8f8a228f54cebd542ba81ced8422ea6b1846d203cda011c3b3fc5b8664f41896542eeb351cefdc836927a8be71a64336588540e82ad648676ed04565b612f29d2db784718c51e9ffcb56e9166ea65ab1e11a42f180b56b37aa8a06b2324231ae8a707535496a9dc82e017a712efe8ceeaaa9cc38d237d425db3c14ca91e3ee6f59868a5fda918f3fea83149f44e5b92c7ea18cf1e6f6bf461da6a3d34c6d4a216cd42c7770f633719758fe934aaa511216c5ab4ccc7809d502128d4948fa1e2a9bd267e5466169c60c474c5ad9a68910edb88e0fa449c4413659ba1edd86aa603b1560c12487e8b767c2a8d90fb0b5fbd674aa4493fa0e79d43ab4b581b25adb21bab62d615fcf47416e8c5ae2a30f43572a80d60f1e98da1a5d340653b898caa29b9f9cea2a8ebf1ababa2adffbfb594a61a685ba5d032e9b2fa9ae32036b57029690bc84586dc87151cb318d2e5a2bfce87607b86cb905cbf0ab82659e3ab1e11a4cea7bdafb77aa8a46b242524155a48b8afb77b4983585757fc2180cf95e0ced695c8aef72dd8c342ac4a7e7e32866081ed7fb0658be9e5d90821b7eec33b21a0e90acb5b7b4bc900e784de6a95ab340079cbd7c85633f34ef53421b636e4f1f3f00d005a0727733e198a6328a38180f091272abb043744ae7c2e43cb0698f7158d3e8c4101cc858b3c4a19e88506d35b80d6b82b70e6e2e1e77ed420bb93be90aaa5ca6920099e3198d1f66b609940a93e6e470f07df3502ecfc40513f025c5722471cb05fe2da9ad6c9bb87a0f8ece351a6b04fe9cf33a406cd305d6e8eaf7853a88416a6451ce4fb73d63c9db3581e3acaa3e04b1b7b26e0fd5d396b3ccde3d0bbb9c1b333195e5d4668e41f88de1320bb242524a86b2526253c4e6814a086760ab805a12740a3d0593e57321250aad9c9f65db0a9de6f364b652dd5bb3cbf488d7c6702e3d81f33faf163281398ff242d82d901e38a15f5eca5f4557fa812469c022c6d45923866c1287a7d73d3c5619637eec36a398001465215b901152cdb8770429e0ce8b7710a820267eff9202a8fa77cbc753a849a81f09ece23c66ed29ac8b7cd4e30e3513e47d8cb4d3e12f072a7fa70bc0e2d6e848520825c523c7b086f5fd4fd23ebb6e7b8b0f2da49fb57c10fb5a97c484797d62c9dedf0904a78967a2b80158d8f784c83ca22eb96f16ec711a9e93a76971c3f0c70b805780f8d2f742a5b04c8a9277dcabe41802fad3981b1291ac8b9ce4e3cb895f3c4bf252625ac6b26272668c3201f1493e99db3da84a50d0e87293eb1fb635657290f6264d0afb9be756ce2849a4c179bf9586a09ef367ee05506a75407bd186c29ddf96e40726d8c862787a5861a128e3d1355bc28f60aea9d743436e2e3db28cb366ace6e55d79b15f281a6f8339dbdd49a3182d39b49dee4d2a017f474bff030f7411f26b5f3808039406506aa6ae22cf14c140ff65f7fac0964b80d7a77fd39f879f97c5f96a46d5fa0cf0398f0375ae6b95e85d6c274f926e988f37ebe79a00a85fbabb76b044342f2b513a672d70f9a8699e25ac43fe2969436a2295033bd52220d0e123e93c270370eea8a4fe8a6c8dc63582be7c208d4be3b7097ad7abcfc82c397199212d55da0b95ca3c37ab06b27282744a30ade9d4f316b2831a7a84a240cb44c796dc03cc5c657ecb00ae947cec2e9dc47043e3733ebc47e04308f101617740f956c5d247bbf8e7bf724838782bc4c8302805fd3eb6f9ca4f121004c0d37c3b21bca68fa9e1665d20647261cb2607db7956ef932607ec49c82b472d7a60fa398336ea45c7699f25627d4c549f922db4891b423095c8345f9642aad849e7c54c7b6248989d2ecacf2e1cad4bf3d9c98a01c9cd777b68ca75792f33ad5b7565b7f20f0d8d3115b11bf593e37915d50e7b98bb4fc285ec1b2d5be01ed9037905a82db91c1a3f8d6b6858b5b18e3085d17c469d0c9a85503db7e989d2535b6884be4c16c792f30be5ebc65674897df3a7bb09b9814ca272827b76b28292827a03661a42a5ae071d35802e7fa8747c850a2fbf2bff2b554f0eaada7ecfef834b82e8709565a1a1cf01dec6aaa8a9fab8390cc3110f672761b63fd296d0a2ca878c02346ceb4a4127c8c0300273e004a7f8c16430fb7f4e78e9a5cedb1952a830e0eaf4112c1f271f42f9c38b0ad648111f37023a24b667c472f02ebe96d99a79d9f47e80a9367870158a56ba6ae36ce469ae9efa075b05e30f2eba5413a12a65135223c2de0182b9953aa1ed1163260f5c77f54ae2dcff5d9040d7b3cf1ca12a2e2113edf47e54ef095f0d8f7452c99ce58d82f5d072aa9aefa6b69ee1ec21dab912d06b94f4a66b6923c1474cb30996f1a99cbaae71a60bf4bcd4c6404b8c2a71011ce7abb6b292a29874a36755a114485563d3915a7df8066edd41c8931841fef86d411164efd14b42ea66440ec87d5777aea3f1077c803facb21f9690769eee2c61f4f90dc805ad8afa1ef831afe40c0c5b55d3e07084046e20a6ad1141c99283f417d476311be56863ce6ebe54354b725dabee1fadf32c3836abe408a22c16812676ba61819478464a8be0b6b7ed7d5fad3c21c8bd1c9ba59c4e66a58e4b25dd0465e8f703e71d5ace4a3a4b72a24cfc7da91e07a099e59fbd6d3db8b242c18e92ceae4fe04e2d7da4ab0eef3aa6afba90f0f793abb856f8a8f04775e293ca879a189a629146a803bfdac4b471e69495c1e044be2be5a8d9b86fa1543416a1d4143f8d107d5ec28b7983b54d2292a29bf6ba92a1290ed2b2aef15dd34dab300e445c8b7bbf1e4d8319eec432e38932aaf7d31a7cf4992c6a2de95f118f1628fbfab124d9c5bd55594752235920bf9a7953f11da91236392cb125418c0fd947f9abd6f14b1a7d82a3dfe8a3bbb2b9330c3f19440e90acca81ddf126574148e474ff8820f72a5ed979202048db2964a6aad1c339f42d2fd5b69f6571ced0544848b366299158a0f0e1e3ebb60ff048e700f869b4f43342a9b807cd357652a337ccb21eee891ab2198a56f270ac3c0f443df23008e7ae8366024e9eee548cf6beb4838d1dc706f14e5a156fe66db1fea50f00e228bbdf04709f4a5b47465ef6450c7971464505c2d0677707ccfb87522009d21f4892937784756c06a4da1d67aa9c36ba82b2c2b91c00b22dc9b84281d293f6e1ff680133239addd8b0220a244554e1d96aed8e005aead1eaa5f4085f0b4a267673d13c40626bfe93df990385cf3bc7afd771521b0024ffc71ba38c12479a0d266e3fb20e22e27e49991c444288587044454cb4728d58fc48938fbcef6ba3574f152b45a8e4e4ac323e3feac09f90137d40a5481639356eb7d2313207fb747e333b82e05c396dd2056ca48d16b48100626b38418958e2b4708fa12a50399bcceb582ac717a09876017701c510aef459e07c104920a7bd613b06c454c2cbac4a3b0f687649383ca2b48417f842bf184da2eecd70cb654198e201ea88cb038abc4401300fc5526b8c55aa9d4e49ff73c6868df38542b0afde1da2b2c2bc76ba72c128edd0f00742d2cdab186b45199c0c26060ea09288b2f16032da40fc54c81bb2a8267a5c13906e680feaf284f8a516c8c086a9faec0bdbb6bcdf1c82b4fc6db35ff754211081bd98f302d1fb16c3024f006769530ebda22ea7f048a2e768a72cd91299bca3e0f78310f790168b426c19248f8aab6418570b303234e22f01a6973554c91dbde8b7ff6a8e16ff4f7d351d7d2f5901e2a950ad511f3ec5387050f21bafe989793b30c90d9f609c909b75b467d4a17db4eb7cec0087ecbf6de76c6f531be13a3900cc38f33eb50fc4d93b364e8807451c4b38c41d7b5d55172154f0795fbcf54a792a090989d57c2b27ae31e5bbd2c5d71234887dab74a13f1ea374153b7f41f5330ab1e11a4cd2d7125de7aa7cb6ba82d865da8b3f1f6180e2f00221c08400cb8c801e31d46040048e68a06134012a19130000686c2a0101061080000028001c4e368211954cc400005a007ad60f73c7b135a74b1b3a1c3ceef7cd36fc592a15744568ca0589009ead72a440e036a48d5f6cf709c5dc477d4532ddbc1aee0d5f44221bb4249a0739064b29656c4c1e781e5f2a15f0a6f851797adc20106cef46da4d110d7525ad1814e4c7032287e55e3ec628f4f934adc39e16819e82db8159107ac5b0b1f7f6246b9d4c46c75702970d72208378959eb888003baf048ce2ee86d36f0f1bee056947234a0f05ba110d01948b67ab522c19f6d1c9737b1ad0b2e19dc320c6e99b46a3920e0a14711d1d945b24d06f48ee02b9c4508cfe866f02232ce7966f3e9d4728039db88dda5b38b15970ce88e605df815167ec3cde08bc8729e47ad37f0efd0594740c145bd0b53639a606a7239f6086fbc4241f01659184d7c5a478438d0a544a2e042df073b69825cf0808c6e046f91c139789f786c5ade30673786bb7176b1c7b241bf1d0cc5fd2bea0c92081bafed9716093a3bc2e38b3bbb7075d71247250b0a0df0c0f6bb423f1059d860c8506911d1182314bec8d9855f770d5e5d3044dd15dcfce3370286419a56919608b230f4eb913bbb205b6912f33bbb820192062ec1605fd4a3a580a50d1345440a2eea7cf01e5353defc13e839b8d54883f116445ab44ee0ee299ead9e5cdaa4c20b7d4ff6223b9983fd1f04bca68aa5a9c62b00698154b7cc3c4806ad20964c622b6bfb6f7828b8b8ef9c19134cb8d00a0e90d18ddf59b825d49f45848c074679346717641b1ab0b8e32b103059e08ac507fe0c7b16118d3142e18b28b8e8c671a79da015c11587f05bdd5925d8da608e474ec185ce93c0652a9982953ecfc1ad06158cb1d8125067dd63c9db806e3ba60cd6a7e02faba01c9dd0a283f9b54830c9c38adab0a842b02e48e006cb72648f20607c0c78c00dea024753b1e524e038b388af1b72b07310733be4a2f2a23881e19834992f9b1798b38e0bccebe9e7a6aeee39f2dcc2aff1b62a261d3c370bf4262266157302c331d16712671dc1bd99756ee0b8c39944b55e70b1d5afa822a01a9053469505660401aa0b3085b5e1629c59f815ded41307c3a29dc5012bba13788e4c67782ef87c0a0b02be87bd0b3ab308f712068b0a81f954f01bd51eb82d78400dc513b953d8a267ae1c8dc699457af313fbc641b1c0ad050aa053c4c49d118b51a4330baec69a689c443006bb723294f0f95418bc8897319d221af02162154539b3e0d7b8c9c7e482a7680b9ff5055ed14dc033c0b2542930f04f11632b82330b5f214d3e71d017dde709ace84ee0396452c3c787cff1698180efb04d416716c1bdc060a542e05388e83f4b9c5fb43118111327ffb21b3344cc5da980757df09e5b84db0283f10a022b05aabf69635f45cba046903ced8b20670c9d6927152551856a12585c7be134a23e5b0364794e37ae9f968ac11a41f2d98f973b86ce08271592a84fdd738b152b543283c0eec085184d107f249ae0de731235aa7366b1c4854a563208ac14ae7c452a076e0b1e6843f9729c80859d6f5248b1a2ca23acb8ec6e3eded1140ae72a0878453701cfe0b2748a18f8675471e4960305d79945b86e38bc5a104d6c92cd0dc0204ccfb076d8decca705016e4367459c59c4f70d0754448faa8026881db8887136e929c841444559f3d79905b1f9297d69d85340fb8319c888d19bf115162768ac70a7428afbd43cb7d863556dc58d83a938bbdd6d37f7a924f03932bde1cacf1604f8df5301f77d709c5988ef0da78a95432af460c6e15fa13f482c0a7f906347a9606e7df626b4d0c9b815e10d3fc6e8967f5554d87784f0196e029eb9e0fcc3d107c8399e356efb6f783cb0b8414ed0dda1c16b71cdf877ae609743be604e8051ebfdd8e701196f838349805df2ffae3ffa5dd6a47f793b2670e5639e139fbeaf4aefe32070421a8f29b1be03c1516910be88f53d0421bdc32e0f9ebbb981d2bdceddf381f18295b75a3c0e003802006400c010002003008e00001900706c0c9ed2dc761e215e896d1100c008008101404600080c003202006000811120a063b4a988fd754ea14acd3623400006101801003008c0080180010046e806039f51071779013c8e50c1bb75112d23227688e81911a343c4ce88181d227446c4e810015365d0d880690b03c6061a5b983036c0d8c284b181c6d639a1fa44f01c02c5a700230400060018210030081480183e3afacb165d3efa0631082790788604292790788604122790728604132790705ac3449c40ca19124c9c40ca1912489cc0c41952489c80c41952489cc0c41952489cc0c41924886b7410679820710212679820711212679868a8db5f4a2c1c20cc6916de7e2a60431990f278598bc4449c40c219128c6b34b4089168e1b89bda48f47ce5150620714e9979018133c4a91f53182285f832e71500240ea77ea70070620ebfe78c30108973d6cc8b009c414cfd31c23012c42b33af0120f130f57b04802710af3a672403483ca766bc00e01962ea8f110e91207e99f10a001387a9bf23003881f83d33c5174df90c31ebc710864821becc79050089c3acdf91009cc0f89e39c20024ce59332f0280d449105fcebc4280c4e1d4ef140096407c9f19e10012a7a9992f00388798fa638461243e7065e62b00a41ca6de8e00e804d40251f1f8f5ff9fdb209e5b958767cfa224cc2102ece203182e182d810c9d25535107ba8729b1f26af2552e63d7b38b1e4cb8c848498faea1354cbd333457d7d2921bfb82e3b6b38a0cb6e996d7f4a32572952710b01e2af814617514578722f68f2a19c1734767609ced8f1c9334a8971df6bdcb61fa234b8547c528e1ab6ce527764699cebc8f748a222e7a8291d33ed08755c6e912a090ce70c2cfdc08e218c0786b2e91180d98ed8010536732489a481f0d03af9f296fb707378835d866da1ba8fdf22e44a00d3cb681ec791d707d39f4a1d9db8984fbaae244e94697000209aded4c044c81c6ed6c12ecb7ed0d82ef03c90edcdd01aaa66435d7c3915a0cb7963b44fe6cd46ec8af479c55eb037b09f40fae4a8e0d8ebefb939f3b7ed37ae52d2e2dd26b2e2f2e91d066e112700a042d8e666e1a724ca508182c5febaeba42c56e8ad7ed023b906afc672f8185086777410511fcf7204477371069e0c204b65061d43cc4c20992990b2a7cf41f112a18dc9f3207bb216a917f7b11c00afe47ad5c84ec3cb4e59193608bf3b29d6135ef9eebd307175a1fb3b9233a56ccd8e22d71e276877e3ac5ce51f8e1cc9f55b1885f6f04eedd179207c54ce83e0db0bad267e9d1ced57b188db7cd442c989df35b2ca9592e35fc3dc2f253009f54c7455a327b7bcdc537d31fcc881afa55580f652fcf51b4b1030ca29cb83060fb1f3fed030c1be47c365395a6ee112f48e1974f31876b70247bb6299e49efd1f242d7f980c4889cb4e8607ea4bb76e92e2f2ed66b2f302fe6b15a9949f53951dd68bca18aa15e5e4ee9122f32da8a0a7f976e251abcf4a27a6c1cda1e10d29651be57c6ded81de9fe3a079e93959ad3a6b1e2d8e70b3eb9af28aa68ddd8bd709f8e506e3b67d7b01bd2830ecf17b93a3930ed00c73f25b4c357b70314722e6d5270bb67943ad9e59890df9bdc930bbcd7e11492bedaefa801aad083f5eae44550067578075944074e7e27cf0fe644de9a00df8297d6ec24ad174577aae996742440dc8e1ee25f73b36136a585d56444a7da1f69349b7ca64ba88f25848ebd79a7145147efa33be0ad1caae8e2d986f9cc4a42000ca5fd79c74ff897f2e814b68e46863c22873acfd9a7451e0614742ed1aaa5e890699e615e444245ed2f302fda6b303130421b0b34b44f236ecc58b9e3318ce0b093dda38d6564f693408786feebc3f79f4daf2f518fdf7bc524d6bc0ce1f0167fa7ff5d395a15552e8bff059c28649d6ba950d14c4646d510d3e126080aaba64ef5cb1e93ab385c63563d4b6c218e8bbf62faa14c02409cceeb628c061a5f986682a3db7687ef258b19b0f292953f09afc89bd49d930c4bc493f424933a93abd95761eb0be95dfed686d6f2c6a085c1a3895a8327ea5e9b4bb691efe593b4e5650d5757f32e7db6600298bcc679af6ffa20dd5cc17ba0da1d61b2334579fde2a7ad51756a5007ac4125685b4faf8cdff88a6e9c443856d33cf91827ad8e5e0a0b84f0f52c8b049a9351b67a52944dea9a94102d27f1303130de6b3132319323b01b72f0f316291d2b74f2abfd90c9736d473125a3dc914eb4f646d6b26fc2b8f046303574c9c3588866e5a1bbc1208b46286e4065d80eb2d241977ff63281c7b10affc2180d72058a1d55a574509d0ca793674bb625442be2ae8d633721393e504ad197579e8649df9430cdf9d1b8212f1c9c00886c8b8487469e2f86ea1c1f1ce9e7e85e37de2bfebef81c5980fbad9f9f5f030be3ae41d2c9de4f3e698f4146cd5ba404a0aaad24f4dbd7d11899bc2fbb0719d100382d41e036f183d65b9e985c4f109836013ce0b565fbabb395546e9b3d5f1ed5535da4194cf4965a76c16dd3404a765a9aaf14847c75f891a17c6895a10e4dbe7b46792bab5edd201f7c67e1f5313231e26b3233320433f72203787a452081ef63f4fb8b33acc73d46a58b73ca02aa6528d05787d68b8199ebacdc065bbc74650cc567c53a5326f847ea3b97cdb942408f454310d6a0238e9a10f9e161e9a4f0afa6971215d5d83913c13f89387d09865761e45e09a60854879ae314f1f671eec146403c9f805ce79a1e5a4efa27df00055854f05f12739d2fb8642c13315a3804fe130eea5bb03142fb4efe448ab0151a262131fc9814d7c682798d5f592120b9c310b4ed48afd147eeb2d855c94f23e23328c0efd5856bab382cabcd1e6804a296c31f72915fd6949d29a86cfcc10e052f2df6a07550830b51a90881a1c0a2a2422da65c0d60126174f1b89e164525aae8e4605a9a990df4f9323332e66b333433d99888d47dc326fed91087796865316ac71863616f38fa0f735bf1dfab1dc1dff6f9b79fcc086633a54d07050b4a70e2f5d0071fef46525e4500a6e407fc52a2abf6ec16c13f5e67544d4e1734478d69999ac33dbf109bb5891eecd58d323ad379d469652a43cf4dcc7807c55925f87ca57f735fb193c9f959a3f7c0ab268282f4d326a425d290b0d7ffb5494f7ae0fb7734ca01bd69a7be061ee758d14b233bb625edc218f4651688855d24942d6fefdb0727eda6c7d037faa5722664dbb7a133ac2a4e4a45756524812276831f3f67b243d76a22f4706fb3184c4c23649e97327d14407519ff93864fa0097f57bae7a165a7956bb8cc81bac99cd98e02aeb94826a9e7807b333433ed6b343534f72dba7516c401ddcb497be4e58362d5f74fcd736b6045033da4d08bed3b16354fa5724684405f75250fe36ab0c920f9e8ba82291c5b9855f7e25a268058957bb2d5e9d2c50bb2c6a38de45280289afff9e2c30ccde50619bbf11706f5f8bfb18db6c1f721ba0cb181aaabfda7de2fd38dfa1ff77fda00a6910d675e5a92005596d4bffb1a59e04388836dea09678dc7a1d5c444cbaa4eb4451630a309a6f28c8713e90ff08039a6fcb8f6c6ffebd4fab7d5f343cd533cc8fdad3a0a6c09602e48acfac1ae486a9b6d6ca6240c2a3fa1b9756512c2250e9fa5c15a056a58ce3f33f7298d75e3a5ef5b4bc4c6709923168fdc8ace3e3ca52b30aad4ded1eefc78d21f128d847b343534f16b3536357f14a3f9d0149c000d41e1f5c352120676c8c7781d929f7900c043a6cdac6834426ce84a6a33e143f1022625e1cd73e10c09ccc79b033152e42fbe6124dce99da07c2897b95d6008a65b701c80fb96b4bc23bb3d993c09dca778a618c6813e7c19a1e7f834916b541d9055875b948ac0977e5d69f87f44296a5c2e1282331031512cc1cd23c875a4567d72f14ef53e5af5b9239bba15bfb837346a4acebb8bc690f68b26a46b9b013aa85c0686c6b3b3774b7191909beda4a7c5f53a6d62952857c04070cdd340cba11f4380ac28dd738bda9a779db637d200a857c8a0ab02bc4e9d84cc00207452c3ee8a76596b1bad4404511acfdb431117eef291a48ea5d9f7e2400d88353635f56b3637363bfbdbaa5fdc85bb9b3bdee204566f975432b0e2b7d863d240878c6ba6ac2c8cc6c1ec654c2bd36f5e98c4cafce0c466d12d2d5af0229080a255ef190980402498301e3420e9b54f8fe680fac8aa3296757243e736492ad2e77a8b619d1a25fc962a95045cdddf6fcc83a9b89166e550b798daa9ef8c6d514024bb09f22d746002d383e3e6ab62ddc4d8f0b617971e875d290172f7bcd64890335f3ff6ea9c7fa706d6896a899904c107867c6c62f4f9dea6621e8f8a11c819d4dc191435ec88d7a6ea41607aee869bda1999c815b1e293ee45e2d0fd3983957864d40c1f73c04b61d3eac353361f1e2a63f8895d38edf48c854283a46fea58cb669c7bd2a35ef9ce57348c7b363736f96b37383719cf406081a474cf351c4d96a6a199f418f3d4e553fdc5e5bdfee05c00fa4e127f7267676b58cfd181c94572a0322547c684eb26bfc99fe5bc50b4a36c7bcfb68c83540d28ed7f0ab6321ead21fab84917e187e9382ab981af90d8597fa333939291a9f9e25f69132ff17d7d0ed1f073aa41771c257e4872548180376bafde1fdc2f98cbe3cb97336e0fe958c2ffc3850825531d79d9f4e4b7ce61aba89f32f2a159ca6f1389afff1f55255571ea6eab5bca12e104888da98c53e8165ea8cbcf8c6f07179778f16925b74ecc187f1d4d888598da77cc4788213d5af8dd65c28dde78ab3e254219509e5862dd25083988b6b39d5c47b2253f73cff93a3c8fee2ea64e43ac93373837806c3839383b461b33a47d69f69d71be2c9997273ed5c1e622fce33a186e03f0e1286d483d41ac7478bebb7b0a453d4cf7ee55adeaad7526efc84ec883c4875a4e461455798e303f43f54731b3c344714697b5f92598c04f6c289001c230ae7697a13f048fea5286936a715745b608122b90ef8b0db93097081589a1f3e9895cdba53bf1b46fb86c573687a9b3adddfbdd2967bb2dd4764d007171e405aa30ad294affe70086cc0ed2820526104c2b50610d5f60b6daab0ecfe576410edcd2071279dc853f5ad3b16868c767a2968857c570abc10da970e571aba994ba77aa72c70ddc7ecc405e8507f4dd5f14f97825a5000799871828c99f6618835942275efc691b85446f4b421997383938846c393a39449dd45aaa0976fdcc505a3773923f4414e7edd23de1b0c4cba14554252ebbcaddfee34274ba2830010b7e18604c02d9a111ffd5b6b472fe9c466badf3d39f0db57c3c637dc191633c53aba15088b570f94e25aa6df167641ee260e30b7201966f85578e0f439931a25017309ded42888883c318526ec9a047fbf2fda1b8b7b38c743d044fa01c6196d940544410ffa4b4bf919e5f66ba06cfc3c4ef16070740b2fd70f1bfbf2456cc434378d7b642522c5043431512219e31fc7a6a8f2be9a39a9f07d543b072313ff0547c938d80e1b292622ed8c33507dd5b59b1fe244f9ebac6278d64c72ac50f43fdbedf5203899a751c4e95991da25f011156609db925a01b766d9b7b393a39886c3a3b3a236633d4f74cc9147289934cec3907b1543b414c482d8001660437f244a1d3e1989ed2137b2fb1df07572b0eecf5f848e9ee82c15f6a1cfbc597dd5a399e0402a13d6a5ce38aefa799d76357c3a072e6eae6cb29e4bf1be0e465650b54d166c97477fa3a0c063f9a7c315cb57135c048ab803564259914d70ab3571dd41cb48d550669954e46757c5e87e7ed1a343cd87e2d23096b2ca927d8b250f7e71187d1818534d3190177cf2445b7c5d0046aa42ed82c9f6875ea52e112d380059d4c441bd419a5790959b65541cfa1c8344e649960463da9240eaa4c4ff4c0b68541ecf4e4df3ae854c5c5e443335d0d9e2332c2bc6f6e001d2ffc380fbeaba0fa2956fb7d4b7c9f3a3b3a8c6c3b3c3b04f77122a2251cfaaa8ef44ed87090871b78c489dda5bf0d7520776ea0ae92541195cb4f7ae86db739e98f350a530c97aa6b5e3799acdcc6accbb2687213cbeab17c55cd8ca764cd8c7777230dea7e601566edac6bb83d33a4d148ecb0b287c49e471aff62facc3690942ba20b5d1f79ab7881ba2f6d20606b153b6550e4e8ebf38b60049f79ff10f9c77ee5bff32d2029b064b2c26d1916e47399a2e5e5c7118d50c0f5a0f0f5443d2b2d2f5cc6696e6dd73eba30bdbb02b934d37490fa4a0f0b6e93fbf99a864d5254068717644ca693d722df0e8e77fc7b98fa4c1d87f703cbb4266207dedea2ac5eb8babdebaae801a7bc7fbe645a9d8e9e8e90d7dfa6746edde1b5a67b3b3c3b936c3c3d3c78683e0890d6f82ea72c0d7e124285ce86f6a85a25e0098a4f3c7297d17fdcf283f5bd57044dfc4f3fbb076fea72e1d917b087cf5dced625b805a9c2a236d6ca9085f8d383a1761454a13d790bd21d6ae1aff9c929539ab655c65ebd114316c0004274cd9f5a275c02827896d9dcffebb3f1fabfd8dd303d4cc43cdc13f34a4680e9bfa4353928a648eca49f37c342561a51b8f4eba9d9a4af0fa780a656613db83cbc2edad1aefa1b9a3592caec08af721835a571f126d5b92e86e4935e3c94a4d7f8bc97af035b071603fca3f6ac13af86020fe2e5540fbb70de992cce25d0a88a7f3fb823656d8719fa4d9f4df8507bd07b5a3e0038b1e0c190a15600d3cba06a6a8eaa7b3c3d3c976c3d3e3d20a69f2c69d854a8b8051153d4a55887ab8b49e61d1dbbe7cce0aa4d4b1c242ab596fcd6dd84c4ae68e9d6a11788d379bb0945bc4fd1a8005274521ec6a69a0294cdc49ac623856f6b1efc082a7fc720fc5f37f9baab90676bddc30a4ecf16285c80fbe6876f83987255245f5d34c904af6f6854c551041354d4809f93656eadb0c89bb4ee97bad4a5610dd8d1187d7304bfae512d96ca2a4401dcf71c655f28826a4d867d94b2f51e68bc5fa470027a7fc5e4657d87c0fedf73d157ce7ec562d7d56d7892b51d89f02af95e5206ceac8957f1605552f164e95604b5a264a210ca494f343916bdc2ad64e202b5761d4cbe746e79b32a1a40c64ee7d4460c388b08a8bbae3d3e3d9b6c3e3f3eaf93f5cadffa41907deaef23ad83bf409a164e4ea6264aafebb4f0311c0cbc7d3334d85e3beb4ce2f9edbdf30013d77d48c38c8918d4da71311bfc87ecb17d45960ff053e455c3d96554d52ff6170f80040ea33b92333ce62a0e211b91dc342e4ea7a579c184b33fdae2f63e899b4674ab49d9425f7e7b5ddf8c44885725b4d2bfd2feea9960cef2d2fc491389ce52ece651e82950df1508849a21a03caa39f5a10300edbf03b563d2dcd0209ea7e0854b73204ae7f902399e8274ca8a027bc9d98f727e790dd1bb32432b3297be6f84819c4292424636e8da6c6fcdf161ee84b308398add5a383946dfd82d5a4a4fdd5e3000b8665f34d12ac40864293c12117f917567b23e3f3e9f6c3f403f10800d11b3539203134a273e50daf19f836b28e42128fc5e97a2db8c5e29be5a092499b6b3615279ac9587eaf25929f7638454e10e82c9d2cb1a1169c8bcedd0afa5fcbacbf0fcbec57d455110101aaf0a32f34ab996c8a93f4a1a5a7e71581a9e499c6e3cb248cf5c9cf31ed0c799a9b02beb5f05121fb18c40b7de0be69f8ce72791a688debbb034f316b848a0f483b129c6ad668f0ac5e9d3f42869e7d71493500bb348a587945cdf55420a9806f7e4f7a9c93cc82b07c02100f893b10c7a738f9cb809b231d1b48461420cd66b9a8d22631ea71ec7af09ad4e10cceb050c10797f3963c292bb3d2eaabbcc59c4f6e7527db11d9abcb55f36beb5c2134f1f5b2ce461b63f403fa36c404140fa452833c39a5b0aed7f7558882abe2f5267be63b916cf838b052359a66451c841c49a910c0f5daf1db54fedbdbbf21ee476963c86483e00da7dc70d12af4267b54e0aebbc4434d828efbe530db75b9bee06037dd5755f6f5f10ac2992c2ddc8d4edcd018e66c0f770ec19d3297d7a5fa33922deaf93959a62153c2c1897428ce0a1957661e2e753a4a43d5dc3bae4b90f4a5608bf65039754b412f7b26632329273c9de66f959ba0c7d735b9dfb4ff6dd25136eb579d39f38be8445f9cd4290e03d01f1a2a9b5f301195b092d7001de8c7671a8b302b6da0833ab2d309025fd62311d506399b2bb543e6aa0cdf2ff71af1edbb837465a4c32f77817870ca5057971ffd8ba7b404140a76ca94142419bfe4f5e102ce641a8efb441769e8decb2bbb9abc21e2abb29d79c3ebad0426f333a30bfbb5741287024fc27d3f1a78f56292f2340c2fa6eaf5e0d9ff93599878dd7e6a598465e677b71c0d6a86f07bf1e128581d9f6f7b35a8c4c55b8925a0f606961c41e6dde55f8d047d772d99e3094ee678d068995e8a5419e8de7574671de200afb815e6f3736fc27f0d04aee8b3cc80d2a5ecd25fb824164506f1fbe8540e1e2a8c6ff98ecad925934e99499165be25ca015d3c6e424cc931f1f9debb0cdbc572ca771abfd8bd5f79d463d94bcba7db991fe6dfbcda04158b595ca702fc154eba85a36febc75b97433f034535f2ef009a0898790acb949ab9e51a85fe5e7ac405634f849e2be7ba9ab6ca84243428f23071d95d8620c7f95f79dcb70d9ed5e0799e92b7d9450d240c005f5a843b3333a3012a51ab604f3d6dfd50fb08c18da2fbc983e9472815c5328b23048917c0e753aa3d0f6c8b78aa540b601add2e25a094de15caf49a46d30e70998f9f862c1c614a1771e69e012532861045c9cff1bcd6180d1973dbeda06ea2b3d2164793efd92827c043cb8c294e0413bf3845c75c5de6ac1b1f86ace8a153bd76d1b04e73fafb10774b63be5944e70b04e5bd29f0f55513f9bbc97b81a5c82fefa1668f87deadb611e528e10af5f2c90ff4fe97baa27aeac7b75d4478b3e396fdf3c3a069be957116d7ded7b8e9bd4e4149f4fd743e2108435790ab2ad661c6c74a908896823a72829b6c2424342af6c434443bb884e46baa348dde4f6368e5c85ee6392c29ab5230f2f88a35f48bae798145e910fe5553d605404406f06814264cbbed687ff620bc3a3ea4936be6044cb9f2caf3d368b7d61cdddcb98039275b7cb6c17969d5546af36300e452fea80cb41748fb9710b7d9835134245be7487928e34b51b2beb854760ddf9e6bd0ecf535db796230097ff0a8d915f602fc0a1fbae474d215c17f79870a18421ecbefcfb3096a1d68045c2948f6afbd093bdf68205613406b3bb2548f8d98fa12606516ffc1a675d08521992b8c6b9b161bdc9858294877dad9a89fd9f547b98b03a8f199025077e2829d3c5dbabba2922fc082196691cee944c49e353353464e4a67708bdb9c275edd1ca7bb76c44454492aab4fd2e792d3577089f5bef118064be2d2445225f489c19e30dec3631152a07fa449e921254d269a492bf75d28bd047ff3ba28dda602ae5dcb2a65920e1e5801ad079fd7311264fb02f27a658c2fbb3c4e415bc6e3e144654002d7b53de08327ed10068891e48fb53e1b31620e8f5908aeefcd8cd0c9580b34d4bf97d8b823d6b0ac273a978774434f88996ced62ba8b785d31927516aff1f74df7958de498704eb87e247519dec515f11234c18911e1f26948a4dc36ab040c71cdca048b3175c12949e9bca7421583782801337c98f2597d0ccecf048d440d06d3516d93a82b9fdda15eef78c728c955d5f25affd980fa8c7b3032ef98bf79c6f5ebf0518d32b7832d1444544be6c454645025b9609c4ea9df614f2451ab4e8efcbcfad41ab000b39775e578308aa38febac64deaf6a8fffc290d453d8ea80869d5086053a7bb5ad280b7bdfc4c353166e4931750d49d8cf3f7163010d3550c2da4a90e1bd70957923f89188f84cb3e42625e6dde538371cb666c012d261e4ef89063fcc0896be85d21a9a224a745dce47cb833818954a060ae43d638bd87a475e49e6862c286e7306bc23acad6e209d7a1c7b21d03bff3464f16dee1637e02762913c04b7ce4170aa380b8b4d9e41c9e9225fa57aa5df36c02bdbffc4a0aea04b454640900aa539cb99dca7eeb4715add3d47da98b5ebed7b17fce3442b93b2bca1c186e4ce23eb33f54223bd3278a3720d5a49cd57b454645c26c4647466cada2025291884bddc980da0a4395bc557df36eac4e04cb725a76857b8f5c7a0cd94bd3fd3274d51ec0279847c5218da889f91ac08636c3953b8f628528c932a72016bb84c6e58b430b47a248b37b1b54a534497f2e844214b7159b316a09582c06dc5d686c78c6571c8e70a7e5e05eb4894432bd7c02e4e021ffc49d83a7981aada814fa904222c77d095428b50fa30faf30297ee77fda288b7b636b63250cabc96e40ef87a28a5e9ba2b3dc7939b974e13ea82c724c7aedff02481997a8c50430948a1dd81fcf890caaba848d1602b042909a298e3cd15c7b420142533a58196a425d074028fff19aaae99151437e2297755e44913ee951bc79d54cf36d5876e97890d97bc66ca7474847709e69d5d78b044abf629cbf6acf6e66e41ffb932d52a99c134437820002e2ca333a319d5d048fd9a3824e561e6ff241f7c829952c19a8c2f37cf0a6799e43493558df86569182fa7f8f48c564f6ff3ff90e5c6b59b234ce1db4ea7463497565b742e63b02510161385f98ebd3fb5224d7b46099ec57ec87e14bd0f1e88ab402068cef26c0fc1d67cd42309f5d5cbfe6b90aab23aa2a56fc8dd4b184cd61a7d3ec30f78c185fa9dc9ff05976487900b83d7b51cb3d88ae9c867d3d26b611e85e1740be422e1a3ec20a6055f1e8ce8368d1436c8ddc46a854b8e3b5db93d04dc373ad2bbac28713498e1f0b55a3c81db09f47e6f68767d4bae364ef9befdad9a55e3660c8e330f2f0c735dda7ca6ca94849480faca24e18619e47e09f8ae7c69986039661987d8f33224d55006d2436eaa2fc31a059e9604cd5e4c5c910a77e2b9e4ecc35c818dd39ae4b1670379fa94723ea01af34f0874d8d3fd410b7ebe8dd3cd6e4dcdd2c7c91bbf67ea95350e241f5ea6614c3473e88a4c3913f4276f2587088938dbc4ed5016f6ccaae9be4cab8112e44b19c29e753f71e4091e2620e690513ae5a9619ae6f687eada0a5d58a1485634aa644520ef61bb1b7f6fd7aed39db9ce89e57767524877fc36d98085a1e84ae4729b15070245f34a969caecac9344bf90ad504f23671040e600928166b907152d54996ef9e97ec93ee5c26ce54701c9d2f1a2da61ca88773fb3e18dd6ffe267bbcac60525e47b484948d16ca8494a49e45902913c022256cb5198038dc48f8b56d10588fbfef6f22a85623978608b25317bcbbcba4cb91833639c1236d2618bd673aff63b6eedbf5b245ca83f2ece8bba94d1ad7bdda12f08986831a95a4697cc550ddb823bc4dbf2d0cffa770de3e119815426b0c70e943138a57b94b8272ed5a5837c45cf132a43b9148dc378dbd6690c3147915b29e0859af80e280c7961810c49454bcd8794110b8ed33558fd4f25aee32ae7aa049f285d0bbbf03f82d49b3c4d99c2a7910019440d6750b67e13dc22dd875cb4af44bbde8f454571d6ff728a53bc556d4aa1fa04463e3af1802b491ab596baa09f3ca39ba8de66da4818e68eddab2afa253b458412369f5845f7c1336e0610e8494a49d56c4a4b4adb87c08ee0326fab3ccc0a29b034f5ae83b0b4f50ccc9c134ba2a181b985f2683e266e2e7047b1165bec99392c3c452ef35e7cc2c5445ed4148378c76a93bd76b0fc0e03a32a57c9507915f53a99003855eb27664f8e590c78cd6cd4282d78201cf5927d9e7564d3fff85bfe7f3385c2a7edd8b1487d5109e69cd23f4c1c54d4cdd3b7cbccce157b43c7c4f52ebd86d0671d7377e137abe79b9213eba2c5fe73b9ba0bb7464ff616b26dc073dc55bedf0d0c80c3b48b43d9b5bee3123699b6f794bbf0374cf00f6aa6e6c436c907192fb6b342d3e6eec10e29717c2508bcd654e991563e221f4ffe64e9a79c714f79442dbf5a955f9d6759de54d3373f6c92ba41341770ec4a4b4ad96c4b4c4b92f8e418c3c8e4a45b90be40888a449dcac7e2dad3213cefaa8e83da461c4c2e3d70c93d952ccbaa84502215e60a6b06090e8a6bce5b54a54d8d6c0f1b92620aab5fcf75ebf02d9cdda82aae609c7eb2ad5bd3f2bf03e97b1916be3ee77ab16dd3b4b61a485559915c205e30287ee487a33b8993215755b58085681c1edd66f5fb4d1b8080be7f54dd676f12bfaacfa41c16edaa5c88c642feda96a29f8de857b1f54fef5d1673f0699ac80c012201ee52169ff4b960ec5518906babb2266a4f032a6264f911a257e95be494af1d549795397eae43cf3ce1fae584bb316a3b77bddbb61949621c50031a7b94d7f49881cf90a04e0507de88811037628e870193222e8177f04b4c4bdd6c4c4d4c3f32d79a3947aea2120aaa7e3d483ba45372bc9867cf4d1d941db8001a2953e47ea8241473ed6f42258b94d69b58eb1e1e2ee43b956a636614b333685e816b878cd69e1612824b2d3e0f73ea47e8b591a0b536b43c235d498e7d71f5fce3e4169aea0f1d910ea1ef572b84ecdc58069687c51a2aa9f15836ef2deb3f7044c4acc13df82cbd3692d388c3fa455e28b4e519ec9a9d85ab8afd0778176cd1ba258582b50fdcca5ea13e570861c903091f59eb126192178d0c7acbbd130f1cb1c1ba5d2650f6e45ab873aba612e3f768f4c089e5f408b27a13d53eb5ead0ef9f3afa31a891c3ab3f5279adbedf1cf1aad71d54f55650bc4f42025e9f5b913ebb5d8e5fe80b62f44c4d4ce16c4d4e4d352b1a58eb7b43a8447b98c5c0bd709063d757a714faff08819777d87a2227b80f95b4b8abc741bcbc5e8c18e1f7d86faaa6195e720a25cd35c68ff30e14c3a2b645436a4c64a549a7f573da54a8f7a433640ac81a4bc6d7008b3a20033c0a1677d7fdc9b941043528f821901a8a3d62a23d696bd730e36afc8f24ae123daf47946b2e7bd3dde7b9a11b1a84f93532819c710e2d272a90225db18bd103ff1a768af67beebd5b17960addbc6e42d5abff46180c0327d211e134f85f028e9bc26c7c0754366c8f2215de3bb4ffe0a11080926e0113e224de33b23c933fbbae14873814b60a5cbef6358be400d0e199b1f49b97eee8b3c23d524c75059320ebddc4c0443f5df84d4e4de56c4e4f4e34be26995b80b5fb0c19f6945f9d298f9a0e6780b6387a6dec815a235312e571a70fa73cc7bd2bf556c024e35c709807a4c0ed92a996d7093df722b70eb4e855b4a315c9569acdacb596e52a0bd701c35e536a8db78c16ecc1d25ecb97324b6a53fcf1f63ac0eba0fd656f2fb8b734109294f3dba7e30567c1242d55f3964d82f91f7e6b96e775385f6edfe58a93ab2f2b03b76f03585c830a1bf57dca286160a1ebaac6de9be1d1126e1a79c500e0733807d92289fda187a89fcff51b8b17770ab81b2877d8127ed3886b1046997068965a95bce17a6610645be6325e9383d4f4e768bc0b848e828e9a1dd38d471cd97ab41f58d96c5f5c2cd3a071045e4cf15aff8031fc4e4f4ee96c4f504f7c624da4f661545ce412e06a1705788bcfb6c0abdf2ab24a31831243d7e8046894180531e52955ce8ff1aa83d5c47837da4c626f35ef9703ee0df0d4ffc07a4daecbaf1fee5ab2ff00ca55289e2b6ceb69b2fd0b5a37aa5d5c0c6c9628141eb6d9d1a69d3001c3fbde0f816cf1e50b5a95b94bb4a1c767528df6e59270d0348029639e179f56b4933349b3fa0f4bfd1ea293ba540f00b95bedf6ad4dd2465dc7b1372b84332f09e4b2329e418315946f7c47b06730425519affb33c8e133c9d16dbf072a4c307e9cc2bfeaa4fea5f44aab6c2db3e497b826c0f0a93c1783b4411e0f410ba6b71c83b7cff04c44caa3736431a8c8d5d63b5c52f9e115af78bbf694a8a121837c4f504ff06c505150f68ba049e47f5a86aa335183873ddc02a90442178ccf3145635e5995e6ffe709e5f698cf71f948f593547c39432ac0710f6fc0da0694a796fe9558db8b7072f3950bafd698b00a2361d6ea42ced49f01ef4acd85731ffdb7c7e439e8a4f1150bac904cb4900cb03d8014ff1387d85d42997b2bf2f42c3d001ddfe4369e399bcd7e0d481ed2c6b059f9f53a804d590242aea46c2a6454cfd204b65e90ba150d31b3291bf47c5781e8b12134f4440ee379a920992d9096fd0f1a54be5712818f8cdcc1d9d92cc25dd613c25f58dc87057ba0cf7051545739e7662a9712d2dfa3bb7400b7be2be7a7e308321c89557ade575c8d95c20b8fde542f06043593bcca949f7f61df877c505150f46ca951525183fc62bd6da9c545ab185782ba7a284f71d8f000de296825ae4af2c77cc05e4132fed6deb4ea6fda834c7b5b84f142cdb5b47d5e39a83e9a001449b0da10670743b7247442de585971ecc3d34c78323f3220ba99e061fb31903ec63ce22b538cbf8a4fd0e5625578fa2abc92d35ae3730f92a93ab7483f6adcb7308b473b85b692bc3f3069a7640f4ab0d78f8019999031aaae49b8c168bf0ecd9c5a838060f675b4a4dbd0f99ec52701e321b3b1d73d94fa9eaf5b540276bcf953e0c31e3d2cb32d3f028a36815715305076cdd861068aa950af5e7bf0d6d5a81ec232017986fcf5c111dbc868ce068536b0d0080e7e374747b19a8b8eb2aedd004406461348b7892e88a88e7c515251fb6ca8525352fee4717b40cadf436215f0020726663a2ac2d139e953a74c7253baa1ccfa378b903025f2ffa17e762356bbc87af77b0d459e9a723e19212c8635f6f39362645294cedc166821ef4278b97027aa4fbb084c7a1c1a1cc1fe7917f5f4fb5e130e8c1713508a952dc75685cad4b759f8425aab2095b8c2f4d124e4fa418dfcc8228018fff2a48d077ffedb9826c6a7cf0239ffb6f870fc9879b367a65c6722c8ce94adfb59244b55defd5c640308b369f8de346d93e476665785bb0d293e1f72271bf0656e892de92a8f7e289f5929dd83688a11f5e935bf04d168daa870a5b81dfcab594847472954f23f8ce8d7af2d89100cbce9251f361e3a8427f673f334fa51518d9acb927c525352ff6ca95354533a34259eab11e7e2e8b1cf24c7be801db5c01a282b56c114d46cba8646b9f7338f8c9f9182e38d7e6cc39eba7fdd891a95f2abe1a4652cf87da34d77a275d4af8a348e50fbf7d39c52d72c7bdf111c72f4d026b74a6194fa7fc83be19d9a80e72778fffba9c9c4d49b55278903aeae0ab709c55d9ccc6fcc818e46b3b4671dacb7e2622d048a927da0aa19d237c645e67786743114d41219e9d69f915e78e0ca97384e18adc6c0f0a3ed4339fc0ea0295bbe04bd71ad71e8075730ed24c5307d2b170e033a9f7e50e2373240ff715f5cb4fefa8faa98b3a0956f833389cdfcb1a1235f8485e0cc77dcd8b3ed9e5b8462da1ac83d02caa1587be06fda3e35470cabf28c5399535453866da8545554afd99056fa97a55c9c3d72f10e1cffb92a9cead44f03bea2c49e0760c5f677d63339273eecdd609b939e75a0337c3e150fc875e7d96f3acb1367ea6293bfa65009b856dd18d729b5edf1e7baa441f873c0f971fa934b9ed98e1ad7d1166bc6b23789753693f6b3612795385b69436e1682b8671573be72594bfd5a06e122dfd0dd7d690758e688bdda234fc28342b6fb636b1e0e64ff58628da2b85de6cf3e6c22b0db0cadb14b5f5351a8eedfc74a65523a84e90bff12f3cfbf088a007f32d3e36c46879302ca47268c9b6a83a6ea6d3888159d8bd786007398a389ee610f151b5e8d9ebc52a929b36c26a22cad733716f1105a6948fcc65aea677f8c6c1517650f3334529d7c5455548a6d555655f4d1ae2fde7e588f4b0b0b5661859ec7efd4d19c1dcafa0ea9a89d6d705777cfdd2b06bc149ca102831a4b0401a1cd98b93a00fb250d296a8db5c9222fd678dc8967f09fe0565e143e8eb2592d6b647d6fea8f3e9f3a4b424c1a7752296929c03de1a7e1454ffd851bafc9e886810ffa8cfc29a1bf607dc14d13c52f214928573d00378a8c8fa1969088ffc1e4ef7ccf7e94c19851e542af79aec3bb18a17f508c10063cf8482370200c9b5fc937f2bb3d4281e59a7a8ce79965681ca178441bfba69bb58e6a43b1cb3e44a523a6439e844e9364d4ae5b4096ebadd5bb2c7f1bc894390e0db2f7312f6478afb60957204ffe397bf49f7a8bc432e40dd36917a84cfb8308a15556558e6d5657567fe16f21f7f81add1b6ff98d8e7cdc654d023b6f20485e8b187b156dfa17b6df92c3d3260edfb93ef053512e0651329c8b082d1f84bbde94ea45e65281d117e49700f1003a922c200d3f1bc37177556ec0e6ddf6f36e8f968d40947ac82bc7823be4237aef287e4f9a49a43aa73fdc178e0c722e802ffb2dd68753e2cb73d5dc82cff360a855ecf9e8dd93d710f23578c6c47b1b914cef96c93a69a73ffddde88327d7dfc6d2355af353c2fa9758d3d0d36278a76dcc66bfd1c8a1d025ddf32b25df2a7fe64c50531e87781c2a51690fa1f81702bdad33eac7803aa1d67a444574aa2fb39657b0c178b8347b69e851e03be1c1c1d32262ad7e3a1ec27e149a964e532e3ca87c565756956d5758570d02559b2226f22bbc96c02710d2ea36f9046db9a27e839dd2699d3164103387f5aad760b6ae02b31b9dfeba8d0dca96128d2b85c9529b95fa6ea345015acdb9916231abecb2234b96cf85554da5477c26ee0601db5e0bb8224ebd79864aae5d7e5682b4d4a25b0682129981b158cc18a565b7b05d9ebf148b91f7949c78516871031fd6cca6e5a5b917225e320d21e919d1db7dc83ff5060a23698d54bba44b99354df063ee86a2f292d13e6b37fdbba374300d74f59bec705cb90c2ad0ecb841e19016a5c00bd52610cf4d5dc4b43eafb83e07ebe9924f9d82d154eca6cc3878e14525743a295a5e020f78b973ab049a3884675d0515ab3fc15a56088078be7b0f91c5ac7c575857996d5859585f881c9ce7cc4bb55e13493e5504fbfd02734ba0c660993cd0fa271650d78dd3c486177f4eef790c7b572d46e2f99159840d797316c22cb31495b9378bdcf98eb16830c8a7bfb91465cab555311161850f13ef91e79585898af59475df2962424f3a8c33b08a161b24318f4146484609ac4db1e0af9eaef459bba3cb211720a9ed3b779654f85836556f7f2a1bc97b237be28cb8de7b74316525f9f650e10ea8ab30715aa9d5815213350f7eef35054a3f5ef570d6dc1712c50c00d8659bd58e7987a01d23d488a90f249e34465aec3ba6ef24ced29e7147e8d178ed9269747f3dee93dd01d313253bf7c313a65424ce3a995e39c4593ba4edd25fb45a8ff3b9ed801bc9b05859589d6d595a5993f6c6062fff880bc55f4a9b1b656026b1c5025e16b70db75a5468270715274902526afa5f9d4d5a603b729fc78e67181e0a961047ab2d30e3b0d2494ceeb7e0aecccb9a2f90ad9c4f023c04b92fe70a0ecfe89fa72469dce410dba34b32908c7372bff2ecbd5aa430ec05cefefb8ee6905608bef34622656086936f7081110b0f5dfaa422c6bc59f8e64d54b595741d256f28727816a1eb589bf951d52e7642a242fc15e1c9841eb8511586dcd117f06a4b55bec1a8837d3d4a7b5771a69fc7ac66f7302f8363f1d5b9b330698676ac9031a019ffe9c1571dc3ccc876fcedff49b68558ea2c791124fee7c7fd82c1fff8eef554e50bfdcc444cd28ef9c3d14159ff08ecb77ca46d5a5b5a6ffbd5ac7f663209f30a32d371a57992c08a39708d74f551b0da6fab021f0fba353c632f05e55faa79318291c249db6e4374726bfde9612c9f58b2217557826aa6f5a8fe7b83cf211ff5f169d660215cf78c27932b85341e1e2f2235d033f0d947b5a77ef845551dd6d25ca1a12afb08b2b7c3493a93b7911ae7a1f3ee78391edc32102f671e42fa66977986cbb5bd0a1078a5e2c2104f8867dea8e30f4f460f9a008be199103dc7989c7e330d2d94f462085c38768def8f8f86095a0eb8ba920652a4bc0aca8108e9de6982098de2f1a3e1672207e6ccc89bb613d33795d540554a79da4aadc2294cda5e6b683e35e1aeb11ba39b51532d5b686a8e48b40363b007127bbb7c5a5b5aa86da95b5c5be3d1016edbb2aa64dd33e0bb42edb9dea272419cc332e5813eada3ddad372f6e34339e7382eec090335bcc654179d664318bf5a3b780b1b1ae6146e4fca1634e3cfbaad7ce0eda0ad54929e3d35b26c8d78a0a53ab3a4843c6351b0584752b79c9f7a5abd88027e2992644985719aeb10ea7af9a21db06a72e8c3d07de2895761bcd881cf104291231252296ed3e0d40bccf9732f6569790e0ce99c8d7ab720d1558b1170d2b43ddb2e39ca1366a709e4b81b7d1f77edc2e69c1728648a0b9f4063eb6a93fec4f820ea9061799d2c0579740b2f40683ab73dc98306e9eb99599adc1ac60c08bdfff9c8059cb31e18e3e7a9daaa523cb4b75d97833f6862b7af7bbd2c8706bbf7c5b5c5bac6da85c5d5c75cbf0019f0b4879c96e57da042ef2cac2d86bab3b98c51651a38c323705e5db343591fdd9d29b9674cd40e9fc465d1bbdca936920c27a0e283eb2310062b6acf582b1b21e26741edd6230054feca8f3a09efdaf3eb7bcee4a0cbf9e4703d2cb3a9d9a1eea1f27a9c2913ead5b503d6470b3979a208fd939c84363c8d9697d2f24f5133c5c5ee07f36ea12004c333c48eb936103816284cf029e385c63908273ab888594030716415796690d6fa7f7956a72cafc13237f59d1a1200a9c256f54f94532e445f8e18fd3587abe5a0846cf9964b55691fb3953405f709833bd65636b57ae3fa84cf20da1a424c5e9345afa4788f997950c8d19bfae6a3bdf7465eef8d7441a8b77c65c5d5cb36d5d5e5d208c155c0492f097396094e4804fae1848f11c02e6449d2b2187cd4ddaba5e8ea8b1b1ef45feb2770eb547c7663b0c08d99fa2a906cd783367ea3816d921e681abbeec502f893d65da4e06f4745563229971df1aabe7b11798145ef452a2ed610458a302ab982d6d468a7b7ecb5723cbb48afc03ba66930fe4c99953520c35ed289198fe19c6c3cd5277a6a0827343459f741fc01b2adbf81d8c97d624047c9db6ffcd583208ed401ee949823913fe5b3fd6743bc9d50284908573b5c672b48b47544eee31cc3e201d5180d271675f36a4a26094db306a9165ab242f88ba5cfeb013dabfcbc2b8c587e655379833f6772d732bee2bbaaebd1f3f8c8b205a920e3df64e9fca5d5e5db76d5e5f5e2469a9561373b2c5d5b18bd2a04c25dc2b4aedc9a95002dca98457cc248a00fc2d285428c1343e1dca5bb9c7f5ac5642b01eacc4cbbd32f453310699a1e013c8b89ed86bb80d8218126a3e87a401b50275553a81d692bd1f07d19da05c386e6d885a7833862d1b43626194e46866a7c1a931d5313e12bd6b1a6d4844f3f8864adeabddf3a7c0da38cb17d6bf3919b5905710e671a51f336ec41a069b367b18961638b795ea44b59c5277258bf3d9a75753d92b093941536da410b810df097368db84243b149fb59194ab9537c37150a59dda9c73e7e86143b0ebbf1e09bc831dd3033620d802f6d541edb359998aea340822fed934cf923f306c1733a64007696d97ccd17cbe6d5f605feef5a4311a67ad1df2215838930d3bd222b2fcfea48a281a3cb18bf69839dc18022d34a18460ee482b014aec5a5d894d8238327cd8b0654226c935561130892cb0d743a75b3b0d1fe4019b17104145bc5d054b052c00293ce147361032f36c1a2af9c6f2e0c678d1db30dde8d7cd0a8ca2945725c1282bdc4aed4e4870b5c8c190fe53a4957811a09759465254dec440d61bc9418241315f2577c8773694e3bc8925c7a7b28bca7ea5f3afbe23e9299bc0c20f6f2147d06c2f35eb747027d1373cf9c9901fadafdd710025ee4c61ee72b6c9cfac25afa51f6d9b469db2cbf391ed891825c481199fc3aa95eb428848c47f20b91d52ef548aa0114133ac8d5318028e5361d57cc26da9606160d147a014c55e5916dc00b377684967c45be806ce981a414bce510521809a2dc3459234e661c5207d39bff389fd7dcae0193d3d16ac8e0338e9037dbe808b57dca115c13194a33f3e14b6d58a9d29c14029fc8ea713b5a18358c2a7968f22b3c0a6be3a8bd6f1a2bd99c61c290c18309cb7d9e1a02ee929b0fde510cd2378ccba4d73b54cabe6e78d35dcc0b7625879b7d01d5c7cfdb54bdc07a0b9cf2793197391725c102186dcb82c9becc7f546b2dbb8da80100aed9bfe422a8ef46d74e2cf45ac9acaa9922ea00b185ae6fa0f1e8f9275275d23e509f5b61c8ac47159fba49e4f277c3a1048907946dcbdb4d6a06966eade1a43cf47702355fc8e8fcb072782d7965cdc606160c96da8616261fd75e76c6da08829ecae76cd185b56efef108c834e4812b92bd2ece3a309f1c935582c07d61b70148e72b5871a503b3309a094a5f97edbbf73d6217d0188d46fd8b40a3d5678f2cf5f27c2525f0f143828a79ae5ec91867d57b9bd07f3d53ad28b15bf5ebe88286dfa683d3937563960a0a6ffc983ba14e2f3ca7a83a8b9a50e044505a94a19e2ea91bdbe81ae2bb066e52938928b0ed9b0cd01024d515ed37a62990b9b4e767aaa2dab05dbb3d1d720be3607be9651ac38c4af8834c9019570fc37d639489bbd393e7d46c073bff69efe924900cad1df9ee4b7b50cdb07ccb5950f799f9671ca86df290cc02337fb998b7ba9918e1bc52532e50b092ff2e98b532104151be0616261cd6d62636204d7c525ff65f0e5fdfe4a628637e3294f2a37b2ea85c252060d1c2fb9336940fa3ec85f3fb4379130160fb69d3c194564f2b82be4f17e651c3ea30782d94fe48c1bcd11000966f39a19032db9618b0920af3b32bcc2545ebf259238122a70a91b7afed6c4207cea4ae3fed6240fd09fa624d6ca461581b7f36efed95a973b2fde40637bcda551be4e252c32c308990adbce10a6e3873d34dea2f750e400e0ce90bf9f2ceb95a9c1fd51ca1cbc71389a0abd7e996bc6ac534433c74ef239aad38b46522712f587a0fb7a5c178c8b4184a69fac10aeebae7d867101b8643c3a6b1b263efc61c4d9ea770b0a60cb9aebf9b01589f44d3a424ac0eb77d5f5c884c6dc2d844de77c626362d46d63646349c737bb5680555d782d7c1ed81763329000fd909f9728b60beb8798e5738fe359d3e57cb88e9e200248868d2f1069015d3481279d64da8f17c90afb84158dbcb8b851d5eeff2584b948c0981435657e2907a5547db4f6702ca8854cbbe9b00f28d789747c09e55bf4287dc0a74be4f398728b9fbe4475e7be41453c1bc09d446d8a50955d50f7b6d7fd188cc8777381e3771e4908ea06299d4f29a483cda32ba5652094f201dd570dcc77737b0ebd0c99c14f5946b1a475af1625c3f568f79bf16a077664c32fcf58315bbba9dd486ba92df22b98dd95895bbe5fdd764fb20c02f7851947a3fe7d4662b81df8f625bcfada3477304dee7a5ccee367c8c28b5c9313c0b6eb636463d86d64656469ce47995e6f81a40fec705d53f325b235803d54440755d78bc5cb2f40b775400406a1da7d2e3de23f4351356a8814d666ae7c45185d957e0078b800623fa057a3618d92459c33918e6446959edbaf3fbb1f742d27f3d3fcd7f716119d35d54e8ce4be76222cbfdaaf96f70f2948a66185e06bf80c4173f7f5d09bb380d5e2a7fbcf3fcf0168eea1f81545fec4cfabe9ff04202fd7942205bd7c2c0e8d30de2884145d4730116a3d5006fbe0039e54c17472c747697e62ae64cfe0687764bd19b7b2e66d6d5269fedd4c57509577b0b1842d58aa0344eba957a540b9f245426b63fcb381a04e1411201fbb004483b193c7bf5dce87db26b33e163c516e32ad1e56fff69fef646564dc6d65666564f314baa78d4bb334b950ff265c012cae033cb42d92c2adc6650616371d14fa88d8c5add739aa82aaed53548bc2048fc8243c3ae4af2056a120df173b2e39028eec9a7673bad3281879c539ee7db9a5075a3f8a9b48f5c2a2b40c52bb69e2c749db9166e82a0edec5177c0361943ad5b62fb47c661e347f394d31c7de11499142e8961a40863302f1e0f080e65baccd8d0a38f97ed5fe6f290a09839de59c7bb39cb63abbe3f4bddda02634ebb9e6f05be0ed7ad2a845b75b26fd40702551b599ad206c985728c95e9ea041bc8f912e980418c67c40c652d4fc39145d529c876e7a3c2bfad5fe5998c9f0a631344c8346292f108a8de6c27c987aa3285fb6cbab1e11a4916f6149f67ca8e36d666766753a4b5f0efb3e7fbc3ba813c589ee91412b4d7df67cbd23093df4f6df92ac81345d5ff77acb479a75c93bb0143e2642ff37d848b71ab20b4a89d2ca93d77c33fd919b229b613004ad236b747249d8b4e625195b0ed7eaf9e8a065b0ec50bbe7964e4dbb22d884cdcdfc25bac0942aca64801d4d0c0a55ce13414e72a5ddb0a0d52e483c018c62fccd2726c71703235ec445d389bba8ff99824efb55909f98aa3aa912bd8c6ba890cfe67de106c074664389ab00ef1b0c6842877117eb1cd1b5f4f488ed23a314ceedcc464fae04f723a3a5f40c553fa4fb5fcb4942cf8fa88c5244d1beb107cdf42782fe240e132c7ae0ce8f6ecdd5b531c9dbca6dee305d87acad4fa0ccfa7c666766e76d67686761d354e63f93ca84689a0b8ffe891dbb87a85e8584f406a73ffc1a6e4b57e80db5f3fce2cfd365d90c0f55d045d3bb8ed9501097eaa38b2d01260b5f0cb6b2a51a9cf595a1f480716a395f09cf6658ad4c59ccc713009208545d0c1ddc4ba031748e6e1115e0c5d1f934cc82b14acc8ebb6407f5be75630d0c0ec31e87f56772bd61be71fb8c046226f64520fbc5382b107d528be0644a89aa703890e094aea1413752c4c9c9215eab13c38ddfe8e723e68a7fde656cc053d511f373fcfa528e003e5847a4f9f11972a36b682e90fba067119caa2626b248dd7bd815be14e6bda3a46c0f25791b338825e97ef6be479d95e910b206a748a17c270f45bed2785d711eea817d676867ee6d68696883e4a8d29931000541f807461f220e5fa2aae10112202128802700180a80087c0a2010000c090429500c4570f1068a3fe27476b1c98e0c3a20f0cc82dc8c9c3f2c61fb545f8b021ad8c3b7b38b3567d0dc053dcb93910b880965b3bb3f88be567eb0e5a43c160517ad66a75f8c838370dff7850dcf16059087aa7c2b1272d83ae06a691f767661e5581cecc15a708b10730d5b326a8d047b9d5d8880bc6705580bee227eae614bc6aa9130afb30b1190c1ad076bc15584cd356cc9507510ea75762106e69d599189a06ff0aeae158736da0cda03de39ef9bc3d6180dd69fad7c3ef79f5fdca9df2e6caa0c0b1e83b7ef9d7722df82de8376804d98bd966f1cd77db614dcdd34d1fe8250e022c077b6055b31944e94174cf4fc80f88fa1e1bdadc156824226315f67173a61eeb20ac6ea04b5fc881be00e0b56b0e50490c84ea7b38b33fad73626ba821b44813b03387a860c6ad48a6b5ff49f5fd86c87c2e3345f4dc4a727ed8d5b81182ca10209e358586a85a20ffbce2e76f651e1021a06978b9fead712a0d520bd6e42cf078407170defb1cd6bf9c1305d4cd0d9854d6bb95c1214a3d5ea068fdc560cc8c81eb88601ead7fa0104a0bcc3d985cff26aba80325a0c3b7ccb3668050ab9a06af56b45203a73f53bbb5003a69c4b700db875c8b47e2d0a9a04365fce2e4a40dced68c1dab2eb1e2ec1ed608dfd27701c6ce1d83532e77576a109921c46452d6a0748e2b72072a36550058acd37831420c2fb58f030a9fffce2567d34c4e958780282b52fb7ecb8f7c03542876c5deb0786a18ec50253fbce2eeed683ba3d6d6ed2376819b87c20e3b720f05d6b1cce21b570168cd17b7ee1b7bcd48ee985d313d0afbd42b11bec816b840e56af950554c95a380a0cec3bbbb853afca78040f942f7b608033c3b2d94dc8a8d78aa2d1e4fdebec8208c470db834255ed1320a33d700d06a85eeb030c90620e67179feadf3105c7c1cdc61f1e5b0cedb5169cd28b033abbd864179755b01e5c65d858bd960cad27a51e67173a42fea20ac6eadde4702336403b50e8af50cdbad6079c2b2dc3ce2ed69cc5831e540b6e113f57af2543d541a8d7d9851898776605d86bedd6230ee04cb0aa4d3c4dd06b01d06a90ea77762123e6aeaae07a70cb305ca15e6b040bc975390fa3fbcf2f6ed4abb232d1ecf314681d5c3670d4ae5504c387eaf242167699d0a2d5d0b3bc27ff7c60eb3aa86c1c9ed0d46b4d1ea3e855d451c91b98d875821870e1e1bbd87a2d31944e4a3ece2e6a04aca70d588fb3a0afbfa5f053ad4eb0b3ae251d11cc240d6717121efe621514d5af509c910d708701abd77a02486477da44c30f8806178bbec3d26b85a83532e77576a109722eab20a88ec5c51b6d803b806298570ca0d79a3c4ee59bd8a2927e4385172d5f43ce15048f7d9adb83045ea1066203559c45c0cdae35799caad73b8a26f9e0ec22e2c9fa74c0bc3ae3a1f92d37c01da075f83e1972ad015394ca03ce2e369af54515b4d5ff0a77a00cf55574ad137002263b9d5dfcea6fe71454e3ff8a779f74f8bb1e11d7922fb69d34c7ce2e582d0585c40f0a7d97737dc302a886556bd712b16866cfebec42046470ebc1c61daf7807ae2104e64d60c3ae35e2d7da1cb0d9c52e864f07111040269cf766e40cb26094dab524a89232f63bbb5009380f7ad0567b415d8022d4db5f01965d4b002182928fb38b1a01eb69035683bb0a996ad712d05420fddfec420bc54f2d286851ff15ee40196ec366abe51bc7f52a016717668ed5f10214d5be15172011f05abbd6019400fb6e6717b7ea505dad609773a35d4b901dbf032ec160ed5a2f8088c4723abbf8d5bfce29781c5c36e4f0571c74d7b260942e26e8ece2263b1aec806f7063215eed5a075002a4773bbbd8d447850b68c7fe15ae40292e903333a35d6bc2d6be33d8d9c50a7379d582b6320b4f8a7b075c42c1dab55e041189e97476f1ab7f9d53308e17b4f378da0077a8401b6a794448bb4ed3d985008f77768a74f9bec12570391873f55a2a544919fb9d5da8049c07aa07b382b768388033c12a99cc1ff45a00b61aa4fc9d5dc888b9ab2a1857279c9733da00773060f55a27c0c46c733abb98d1bf6dc36747d00a1c6009aa402e03a1b580f6557862ab8ccfbe7f051e642302ace403656e74494458201b31f34c0c08ac58dc229c2de76427c1a3cfb641f75e6b73583ea7c9f3fd88c134e548b8fcc3082e504d923c5d1bc5b2f4d891ec4ed4caf7b48810621ad329c5106b5369675860aca5fb336a96105aa5d2b59ef60d0e675301efc10ac9e1ec354ce96f0b613367e41c9ccbf61903f9f7cdc5c3611384a0a4490795e763a7965e5e0ef61204e7742d9e4ebd40ca32754bb236ddb4b3cc0eec7334d0e720a30c8ee212d0224b76c0a417a3d3fa57243a739ee732565dbee3de5c560e2ce961c1110edde2fd3afe41334bbafbc54d73981c03e1ab0c1b6477b506b9f9eb84395d0ca709ad29aff1ee646157ed60f0dd5fea8642e2913c2a94b0ad73dbe48a7ad430ef5208923fdadf22ae1f3b857d686968f26d696a69a0c534a0071db00b184d3ad6045f9ee1c5e8dcfeebd4deecd81753c80f3551518b7cb0993f1c2d09b7913d147f4d399de53397e1b8408a9ca755166cfe9bd7ab85da8b8e2708389dbf33eff1c64cd14be0fecdda7e380fcedc4a6243e41f2fb4ac1c84ca4b9884d1351a918b594c4d7789618f53b2edaacf6705e3056849513be8791b7dcb145e060b5a88b26dd7f8abef62ff4884348950bbac106a0c8541d0af6c054f8ec1eec1426aed6b75fa053af133c6d3074ae7c9f65e0259ee008dcaeb39af4ecc4eda2b7443830be9f1cbbf915b24bef459a18b4f665cc8bdb8701bc1c46654d33e305b8d67382c4bd388f649e715ed1267d524ed997bb5f19f9cc5476ecbc78c7d696a69f96da96a6b6abf929e02481c7358d2f9f4546d192d1ab8022f451402186fde3a32aee89a849f34420ed23a1d0746bc44f3ddea12a6c0d81dc461413b996acc2deb18dcb6206d64951a8c9e53fc8252927733be181f0323f75b4bfb042472b9fcc1ce36313585d41ece99083924189a831652b6bec8b54d910c183ab47bbf05c948c6b540ba10f99f78e7c6faf010e46772d74f052ce27575fb6b35e3f31bb3f470edf18f029c4aa9076b036d51c43d1bfa8da138129a75e960dcd8a166f7650a7fd5e58c989d66b9de81eecf1e68497f418a3291d8c6b48989aede2651fb1f09577f58b32b263a625f01cd2c8708a82d284e68b1d3f2921f56b063c1cdb569f51e48b0bc48930a5cb8b3b7907d6a6b6afd6da86b6c6b1f269f0f45cd2e368e82902d96247113b74da86f6205adf1fd8cf2365418d275343a3037ff06c6efc5ad88c6a873f6eea18e7264f76018a47117e077d7b8ede08ae43bd08baf03d85e99661c43384e1370d9af097bd8798d0767665ddca03cb7d8afd51ca652fe2f6f088f06e274d0f7b97aca498cb87ab3ba31cd1d53fe3d684e422ac3c127da576fc16d82a598d901924d6038603a65e132bda80f727b5f6fcb6ce6138fe61108fe29938d001ae3c52d5ccb8b7123907cf9b7afffdc7edb6f359a359dba78733981c1e30651e7b0e344e9ce14952390637a3bb9fa82a5620fcb861b960836751bdfe6b30085fc595aa75aa16bf14bd8d21a4c10a3b2132c9145cab91701445986976b6c6b846e6c6d6c22d1747e4cbb168b51eb5f1537e3143f6f37616ee900b1ad6676f651ba70ba9ef25390cce849c04404cf904fe12a435792accda4f590726c22d7afe6ecf001d19068b12bb6d6f94264307898282c55d263de3f82f95838daf558cebe8963ce8e518424a49482d712b3039c51062c18328858bd60b85001007f17f7cbdaf2e6a210dd4e298b217ca0171fe5813ca51cac1443fd42cdde9309c7e623958a7e759e88122db482da0bae5f453cd95ce102cfa47107529220d46debda705c354de2ec230862a3b6333671acbc69577f9d631788ec13a26f39f9bf560594e1c528fc29dc48f29d1ad1b217bbdb154eedcaabf1febebce02d396b253f7c7f9c166ee924bbaf090c9b6c6d6c886e6d6e6dd752a3b69c2dce554e0fde872a9faf54089ef9d8d2da32aaddacc9d176718587768554e897ead33ef9aa4524a85a055d68c931c1b05dd34cdf917f72aee59ec1a748ba7f9a6619215698a74c575b7aa3043a71c6a2b0a267228a7ebc0aabd9f629aec59db16869b4179e9bf55d4221c8865ddd1f274914fb8ccb21da6fe3fdae875a32109a9c8dbe975d4e9f3caeafbff2a52a5dbb0c6c6c1dc62675d6dbca05803d16f20f905d564c2baba4ff00148e79c86461014eea9ca7f520f745ad12d24dc96db8c78015c9fd8481bb2fdc2a8394f16e0c98d54ac0081763b534ebc2a718d968f03f6dda3368e973e2c6e2509ab4735fc2645f91a329f99021feeaa42a109ab418a26d6e6d8f6e6e6f6e47f4604d3ae1acf79aed4277020812cc9465948e9ec93b3c0dcafbe556214f052249226c5f1f2af3e1fabc42944dcb0833066d91dc542bd20a0e15f3b7023cd98157f9a4eecbfa9faf085771f8dc7d567f23aff6beaafdb37d3069b83d4c822c5ded4cabf3be77c7c20af951b8d52fffa5f66042cfb666a570d8ff929641f96702604d7e8d9628a569e906e2fcfad244d66c08875b7540e11530366f3a2935c2b2679dbe992956bc940307120a5000751dc668bd39bde84c9ec297fb858f5ad866a4a998a279288535579025e8f35aeca7497611702be1465f0c84dfd207f70d6c9992ec9cb0ea474b67b0bbfdff773ecfe922ac26dad9f06e7945e9f56848f9e452e420a97d6e6f6e966e6f706f95c7330cea7d27bf4c0358f22f76828ff560904a1603d98303afec199a97c0172dfc78dcb7e500cff5ecb68b3e869c48350385a1847297c0a0a9183d3d17feada7febafe6fcadcd1d0498a671b2bf9779d7102569ef02641b86c330602647cb90f77c6041b895dd2094a83d515dcec3f826d9b38d691ac0eb7770714208348959f156643bd9577b5e446dadd54fd777e6062444ae7c08da5ba2f9a61fb1f7353aa797747a4b4cb428b28d750b81fb80a379cebb4ebac344341e2637858cd22c260e85195775b4d63f073510986408fce906304dbcd883c5dbfdf35ca31c3d5ee8471930f2e781e437f1aea983d83e188b8c7ac35328ba40f07bc42d36b18c26dbbcad15ead6f706f9a6e70717013b0873ad6008b7cc09fcd4c13d10d81e36675d6a79dae642ca614b2908758fd2f04e89e4e41449d01dbbf86b558dd82cc71bce5da548de69b186d37e91234f8a1b7c21713f59ca7fc330f0779f1992a8bdf1e7da0877ad19987b4489748827f1bcf12b67f7fb58560219c0b6406fb5d9672e2406b3f5d5613e4957de0e6d224fb95bfb510b6d35447afe60e7095495defd431c1ae3c5bdbecba00330672694289fc2b907ad8c1f838e74ed73d56fe4b66eb310649855399ccce70434098e3ed788a548d659d60a4cfb67696fe499803a582f7e2b4f0c0238f9b1e67233bcf104cb93b51a7a769761fe6d27f01574c3e2fdc9bcf56fae4ab6cd854b4c63ff0dc727673c9b4707170a16e7172710d6ded8b24b46e1241144babab1aec8fde150b85cb3bb598ffd3e55b10ce3d3b8be3ee3140792b614b668d02b7777c90b23d1f120178fd7eba261a8bc513c8d98661162edef2211db62dc8d4c0e34e6652b87f471aeca572a8ce5caed5924610f9982dbe2f5facd20638dd44e52624df8d09bde1b26058b046efcb04fc4d6df32f74f405045bfc047c2646c83505e390cbd6a845964ad83e082e17e6b0c4291daaedaf38aaedb00e46dceed90fb3be799d223be764f02be27a62244feb55bd293d311620d0a237a47ecb1370079119bf8414edefd77fd9a973f435e5c768651e16c3459e2d137e4fe0e3e0ee2e2670c35a0e925a7abe784a0f4bbe58c67d8094fa9f05b87d717271a56e7273727ebf21ec64b9be38256f92611f721a18029c7a19c829236be843e5ccf63e17d295fb6ffda4b74e61cfb138c5c0cc8889f2b7cc6de5f00cddd23db2c96701b10b96315ae1fd332e299b42dec386560916e9e5d8839f603dad8b02df81e35e53421f43804c96e4c981d928c3bb32a9129597eee63a7baa7d713e233275f20b83e192ad027ac91af0f44884b299d8f5b18d98cd01279021f20fce2774dd2b9532a79373a0a477d8378ad5a3797106a23231f9f17a1d95cfec7357e7bd9f1fc6bf3458c5e37dd41cefa7902ef26c4f819739af70df2ff87c84877dc7398fa27170d49040b022e3f3057f368b539bcbe597d24a54924b800f698cea3d8890dee115210281d193bf727372ac6e737473bd29839c8289cc9b2106b89b7d140e8658254e8522d13cfdda5212f4f3485d3483aaa10c72a7839f315272fc6b6c9434026b00365e6a32095b5aea4caac85f6182bc7afe590901b4c8897f8fddd33459989a18eb98b8778e24d03293959238d83634a3dfa400b2c3de2a061907b2bd1db9cd387ac6528ee59bc18522515590a4b487cc589ab8631d27321e8f5df17d1f09cc4319f0d68230cbfb058085f7c0969607239dac9232787e114ae40f1643d74ffb6365bed2eadda43bef27694e5e44c0375d7e5edf416af6298f90511660c6b3c39e9c6fd782e2d18a1c47e54267a5bb1e59d408c808c9e8ddff12eb0ee0b103aa36b72a76b6f61ac608e5f28e66698709f8b1c3737473b06ea9747574aee77e983b2034a7b48574c7d8039d36e5bc0720609fe365612f0f6f4481aafad0d16968cca6ac0d54fad5a4d6171ea56e506787dd853bf12af74970792e82428cb073b137e3e6a2ce46358571c1d6c4425a029512a787acc1d31d9fe1b27546303462b97dba93a9056e071c04d02548883d7ccff7b7bccfb4f4f012c0a4aa1de8404eaa6153615f3a5901021bb595de632c8a94c1ffef7a2cd650d0423a418f9171d9c2b927452daca6c7e1f65c2a31c36e18ca476fda279eb50316eacd0a499035f04a57cb59570cbe79d7b58efebdaf6dac04c93d3dab748a5063a954f37162bfdb635f4623adee52986d9891665ee54e2b633050f99a83a24882c3ddaa1de3d8f599ca747574b76ea875767530c8ddabe351a2d05076bbb5bf6417e8c9c8b1bcab45e52df43d026bad8bc2bb323219c3a1da312a472b7f01b8bbf7f51830775c2154ef13803f64cb08e86dadd09982b6804dde2677098d105f4524e56ba00677ad17c4ee5814cc1ee41deffde5b155543012e55ee7500b6fd4d6d6aca86fac91d856a6e5fffa1f98d5327311e54f8092f44e8dd7da39bca3157e37092d46c55c355bb75dc33517aa73f491033b2a85b4a0e9b55df6f1ce8cbc5f818894173c24ada38cb2cacb49c832017ca8e2828ab7185eb2cbfa2e56ec7ae32c79f93e8914d205f89431dd1b869657ad6f996d724c3e7020724e83cde04484efd87b173829133a1abf015a2da2ac9f3ece34e18e0465f2d1757675be6e7677769ae2bf97fb16f2bfdc71bee44eb6c3a926c65166b4468bd5271209ee409a8e9b38ad294587d6182b99c203a73942e0ed4c06a2806f2a0a283486317d3b141fec916c88d1ecc561f1f0e14489f83e47f037d4913da2297975ee113065227e84be95aeebd3434a57b707c7af1a3455161489de5bcd230d794a9e1586e6c601a8c7eb35f42c24d9f8e1bd9e53793d5daf816019788ee2da1d9af3edf6a79755c188a6c8975ff6d0ebe1a4b0d6321149c186ea55c11c03809a4bbe0bb7ebd62141e8f41f6b2774dbe1778bca299e16f001b9a158f21a2435b0e3cb3c3e0175be2a172f96be6ddd87eaa804cb3c4ff9f7894baaa1ce0c7ad7be10f238d71c451214205c8942c7d87d767776c56e777877df822e30ebddfcf3a36fa711e697c68b58835a116fdde69ae460147e3ddff425d094a76d206c8de59dc6fb142429bfbb6545d96303402c9d847db81e7c1ca103b32ee32b93b2354e6e17bb48cd053233839f358a3bc3627719a283d3021c3b3854d43f2c7791bf98cec89b67af297009b51674e90ab61c201f0ba62f6b2cd4012738e610b3fdc2004f87a0691eaf06dd9da31b463852396bb54dab32ed980470afe3c9f63f2bc6148e48cb83f48a91131002e4bd7fc9eae26ce1af16f530128303dfe0794eb549bfdd73bd30214b62f782c90f960be5d1baff0e87db31e71867c971bcb06a92084c694d3198b027a2b65e69e0b629ca8d2d7bdc42d3d5fbd2ac59f36cf9dc7d777877c96e787978e2448f6453e734ca027acfb8c3b22c884455046785519139af3b2c69c7171335ddb1afda596e6273d99f656cfcd96989c7c8ad853a48a63aa911b3c50adb2d828cb4d673f0a081b09a76000db13ab579c523653155f26f917d658c61b94f47967143e804b5cca145573ede5c2ae8b57183fe14ae5cac72066a37f6ba67b618baf5bba0530a4e5d5637b4034133b95de88454096bc53ceddf456a1acd2abf0c898652feb06f0e527ac7224876c788949c316c9cc5f98e55b2426335478fc81ea2c2edcd1d444cffe418cf01cdeebbf64aadf8b595cbcedfe243e019037383de10e114e34f5ae27c032f7f2bdb478d90879e595f1180fc3d033a1ca6227e6f45019872a44ce07d787978cd6e797a7915a990fb9f638812822b43570bc381a59b58366d378765e6de94e4ecc2c8e45a1e9f35d5ddd28b3bc323b9abba0412af81f296c1399c2012813b223f248c7773aabe0917846afef2cf97b48f12efae21ed917e7cc58c46b719cf09917cb889faaaa27a48e81b2a89cc1d620f3748e5b081ecf89a57d493c5edb694db333314bff656a46c95712f9667632e4684e14273f84022498cb1315d7a59f80a04f47a689276cc3192265c220a2a6ad629822579a003790c332a5f4971fca426b6fd5a09f622898dceb7be12dd81cf4ef19387bc8276485ff216850bf0f2642886410f1b4b1be989b2faef65d4548fc417bc4a603ad7c36cf670df00f4257e025f6dc0cd17e60907e7797a79d46e7a7b7a62d3cebe559aca7d847fddbb8d49123067074f6b46e7ee4ee8ab5e2cd1379bb872f15fc8e72776ef1bdb2be5177f11c11553dd2aa1847070ec095e159789f1ceaf9ecb35ce5a61eaef68e0f8864fe3ffb9716a74c312931092da16673d8c88b5590d9bd92116090fdb6aa127e04ec2a3b2e94f499656b97f308ea95ac0a913b94cd1f5e175cd542fd6d5c09d78b85483202b66d49f5d9cec235e82910ff341609429a5b20d2228178d44fade59f2b942f93a3152d135db28f820056a79501455d4718b2f125a0f51927595efcdffc62fb4380d22ad3f1b35ac3a1054272b880336bf3d27897008106c4ff98990d68aecb2095a7e04cb04de3c86675acc8f9d54a50fd382eb7a7b7ad86e7b7c7bed97709859b7f3c20b893e8ba55d83450b5c8c26d1fccec31014f282443bdf6b7362565dff00b7e6d2715c67dbd023f60316ffca1cdf7acc2e938c1c47f9f6d093a9a52984b775f4353a72a9612b62bd91bd4f9d2e046566c0a28705c2d1137aa7cac3413ad622415868ad02046e362ba6f0f85b0e00653b28c0e3977947b9f51cefd07f66d32f14c8885a8506ab76a883e3e13399e471250391c96f9b4b4d378cc772229550a9d1c9d03ad66501791c389137c162963db8dc51750af73deecb890954c434160a0f18f673037b12ed0c85204436257b77ae494a4837fdd739946eb5cc1334acd70b77dc4c4dae43442393be64796efc20d7887b3b5f08b4381eaf7df27b7c7bdf6e7c7d7cb6cc6962673eb7a8a1f04e7de75f6943fd41c1158002133c0641853209fb1e457cc66c3625ec07556963fac8bc31a6ddf3bc514ca241d0374d5455e38cff79b18206c390ffdd2409e99bd8460eb461707132a47b78cd2c19371add106d1a6ac7d32d749623a9e5d975c4ba0aae7eb4f4b73c1ad4211d37f645c8b119f63bc3963a0299ab358deb096ea665fd7cf682bef6b5c03f8bef7842e10160322bc20792a9273b0c1faa6faefa6ba110077e62afdc52937cf76a77ce2bd0a7b84984f909f5b848bbd5e3b6b024d3e14697da6feca67bbe145d122920e017d018eca9cdf6f879e989c291b5634518ce4c9574f020bebc4f4f70aebf9ccd7b9d2a51553518d72ff94bf97c7d7ce66e7d7e7d8912d7df71b728a97959c07c89dcf4c53697367149ad7afe1c777fb4d10755d26cdee2b14bb8f6b58e8f71ff322531877ef5400188ca803de857c873af796c648a95ccfc7b523e856a7058c004f046aa1a0f8b5fb7686f49fa2c8fda445ded278f26e67933cbedad8e193df79fd216e9b0d02ce39899e9da2c7181a57050ac4ba4c4fe27813002c910bf7e9a811fe6d8e21f4c7e79d3ead0aa9322e67c02adf7920db65aee002d1c936207fb837b483f8f2102370895be46c6aa6abef6d2c11545310a908030128a29cb4bf41f83f9aa813837ccb10b5bee5c7d74e7fbc7854fa500a57e4e581fbb0aa3de69210392254d6d0c2b0181e81580806b21046b36f87f61d0d3fd7d7e7dea6e7e7f7ee9b02a0cf5ae5efd4db210f05b47ef22b4cf28bbfab3a062741b3201cc4d5c7b15c80447e62c9749048defec80a556cbc9e39c0e50fb288ab678b166c3601262942d2e2ad2e7d5997cf51bffdca4b2036fa2b21f24abe04d676edfc590c18967b91918d6193f58b848d38aca4e1a395bb2877d0c2dc31929a51d5b43009d6965b358ccb2a51497594e1dc7c4bbe2ee20abed4a1436e3324bbac919e52bc18405848f088520a7da667180fb86ad6ece5a7fd0a278d3d04ea452b13be63e481928ee452235b6beca40b9d0315d75da6bc6b3292fa5c538e3dce2a0e20cf053bec35bfdd5737ce4227b04ae5633d44046652e9df79e96e1285e0528e06db7564afeb59a933b847e7e7f7ef16e7f807fe438ce6618873ac6d853b5efb28d8832b5bfafaf3706f836188d1677fbfe1b63706a799b7d1991930b4f7dd95d2ceec7b2f2977c6ac0c6cf6255a47a2e792628960df540338c41088de37dd53dee458ec5ad720f38dc6b14cb16b22a64880df096bcae2b6b333b02ff5efac6971b1c90b6bf2db0e7cefcec03bdb901740ab163703d62afbbe844fa2246c49ffbe7ef24332dcb6c61255b18a7760d204619e0aa833fbf8e8d8402e4c0bdc2636a4665ba62124f46fa37fedda31f20a43fb567f59b6ba20c2456c1a26ae75221184b9b6f873c21253ec227373aec35db8008ad9d76676121891a5e95df965427d86506438ff26105900893a4ae550e93c5443207d45cd473887e7f807ff56e8081809782cfc780f1f2972e6801b8b1743a0c7bfaaabeb5a79d3500dc81f4aeb233e30e3fda7e90eb4a937c5ecfc160fb0cf44a6a3673822230a9b302ca103a2a2283999de38db9e7390640774157c6b9c34dac22eb3a88d04ec55822cb988369ec8c9013577cc181a9780ed09f9eeb5202cab71dd9ec504ee6bff49eeb38356ea17093e675b80a0ecd843ce41143a2ca446b3624dee399efc025bc08e522469e56cdaa7d240ca829cef52f989f3ad20f81da05b3805da6d98a335d9996febfc44894b4a2ce82dd36f375618f599f763540c6a7466ef151db3850a029228313aefe7b39c222fce783e2b3a6cf76534966e40901df1d84e1674c87cc3f0b930644c256e04d3deb8f7efc6e8182812c35191bc76956977c2f3d474b258ff716ad10c08ad3987fcddabaafbe841a6e51cd9b242d7a0e579b3073ac95f1383732c57e6c3b0868ea0483cde6cd46c93495299989425d63243b7d1638f51e2b5616749ba2430ec75b99dc7cd10675bc83c0b2050c09e4b6c2b7600e3ea83742e68cf2fe43aa86dcd88b1e7c010ec9db032421b974a27837b326b85d69ac66f7fe9555f3388594cb36de33259025d7bd70a1212169f103558e4208226ce373bdfe32d953f267ea0f2774db65631b40e6bcc6c85e4528dade976cdd60a931acdfd8aa422678c34a0b4369d8efffdc5f6af746bf1abf8719e3eb992582747b724d50b74626b9cd274489bdf5de31a25d54c6c8f2413297818281846f828382899def0e9fc956f76fda9f8a498ab6fac88a6942c4ba239b7571e0502d524b4253332a4c1fc461e9813436042c06f680db0eb2be6da292edf9e552133d2394db96f145b5170939fc9492eed573c28e76838461b02004bd06f19247c0b8413636122f29d1b72df14abfeb868eb62bb2a65be272b9b4980182644640cabb4fb130332e2bb18c3733a6ecb0e46eabf7b56679733dd2a5e6495e33e0882f41a18092f9e3f72b84b2cb4c10530a991686b81e6e0156e56128a8d9bdb6ed939de68fb677867a90215614e1db13c65c47cd73984230204c3bbc6aeaf8af05f862a22f7963cc9676615e9f71b9a3b7c656f99a4ea800ff77af73a090c24c53dfc3d12247aaeb009b7e828382886f838483d157384a46ee60e7a5dea6a1e458db7d53a18bba76dfcd8d0424f9ff9290285496e9a78a1c69f945709ed6857b0afc7be54b20829d0ea138bc1daafa6c6e8214aa59a58989bc3ee8bd895ea2f4ef7a97fe7b897ab841af06535629dfc8604786457f4ba38409c0bf2f4cca5033605f9aa4aa4e0f3ceabb054fe130e089eb5f36deabaa97c2c54ae1e9f84f73286c6a031fe90c62e899f81ade14af18c72b04978fbce98f52547a40d7e84ba2e97e0dac55e9f05240332a9463f666a6979f67746e73cef76c9e457a2b04e4a9e9c36715adaaff687073401a3bb6e2291869d1c5ef3aa6e497d6e230246fab1ed1d476cd062a346f90d0c8b89b939839409b73643753a315a27e8384838f6f8485840a4891afe90bad3e12388612df7dda21722db1522dd638929c3ffb3b357fc14b0a436586ff7343a712f97294c483bd731cb8354e99363c6342816b9ee4547d2ab3dddd33b9cdcdd66745b4eff17e6b9baab13bbcc3bda8da17f61b38dac08120d089f792cd28ce3f721df3d761de7f93a41e99beff3b903db9a5ae5eda3a900fc326ff4d2591851fcf8cf17afcb434db166ce3bcc206fe135c1b35774b6d5bceac07af931e10f155efc929cd6df8de899d5d14f05cc300533f4276492ece9560828da1491a8a289de6eee1bbcdbf72578d01c798af0983213ce29a4255b74a9fd8245f871458bbda9bce1fec00f81a9d0f869d62f57225cb223f512949bef3b24b31b3d5a97e966f8586853b87d29df75194f92c09b36792a54002b6bf9c90e7f0ab71fb052211248ac263f8a065b1c1baca709f8a934d0e5193998a2d9bca4db470e6d03a0a4e9ab866e3a8ad566b2d10371d32edc63a3a3c0d6d408431a0a86d1d9552a78c8723efda82f3d1e5e5e992dc62a908e973377f05d0adbbc5f3096d4882dae8507968c0cdda50e2be1bcde0c8e4b37e4e3c43bea5322460e3a42f044500479bdb83df8dbaff954418d4c6425289e7094c31783ab07d116452521d748bf54d502e2d35fd2006d0a6fb1e890e3d04d7f8a08210239049aa3882eb48b0c799e1d2eda49a36c5fa0f70d0e1ead654fa8f06350e42b88b4ef2a4bdbd091819dbd100bd814296201fdb9bffe0ad8586859a6f868786d350784b49d09d71e906899f47ce0962201e9938e9b9e2cf7489f95964d6b502502fae69dc364c8292a865c54b7c6d265a83069a2da6a6fc0ad5616576b4ebb38862136d068974cbaa113f98f09a1d97db65cbdbaf7ba53899dd1f1a5e26b599af52cbb454d3bccefd218639e335e2daaaee2e87a8fa2027fc5b86d66a5800b4cbcc50d8449f67b1be79111677247a410fce0d9d9280b0c0f873a690c9f6f71a971f87667d4d1b55f6271d531f1fcdb87d15954957722c95cad940a99d629ebb9623c7986b440dd0804454b976197a32b89cbca0b6740d2bee6f26290d3e977a5059acde01d09ad3dc96d869646606323cf212dafc4aa0f2dcb1495bdcd5eedeb41d8902b47e868786a16f8788875d3490982b5f46e02bb2bfc447f4795b6563234a54d9b7e885c70efe8504017f54809e89759c365c68b27a697745e9582bc82aaab6e9cfff085a1a58252223e693aab7f7afae37820476cbdff05e53a627844be0707f25855a220cc2be405af77adcef63299ef2e15d243915306a7f6c97f85138ef3aaf7b94ecce89d845786db8ad816d1b07f11b7500a726a29db1b786c53c6300ddbc99950c99f24d2c6df6872af1f87ab705a9eacef4382e207ee2e71088717c5cec34cd0576d9019f57eaafe22103febbcbba8d47bb7354d25f01b234c89e25eb776bd3260ec9ae10ab6686f60d68b11aa880b11142c29f06b9614e08d30bc5ca988fb7f1c774e66d8077d5b7ada6bb7e878887a86f888988886881a89ccb6d06095fa4098ec5954ebe783cfea6154beb054ee6c37f9c1c0a3850c516cc4916171a94673166001df719e85343375b6a00b7f8ce996f46e6aeabf5cc12ee443d20dccd32faff3871feda62579dfafd3510bd7ac57e4be638dc7bd93f4a7e2df475ef2ce98cd834b142838f6b9f5bc9e8a3dd49d43846a2997d078ae99bfc2d982243e106974b752f1b505d51d36443c338d778a71e5b00bf8f8892e1f7e516c44dfb7f0aeb47e6298871b44dcd3a985474005eb6e9cc6b2121f4e9353c7e740ed571cdca109cebd736a1267107e9fc14efa9e984326b5111b73242dcb391cd8aba5357e555f02a8bf3228e239eb18510520c8fef799f9c48b2f453ea98c27e888988af6f898a89b119938193c278444a12ef37453af54e1fe2353d304816ea0899b7c2e88f82afe7a23dd938c6cd0304c07f6e21ef9ce9c44faed58be6192651ffecd9224c395592590a2812ce90e6cc456d6bf1f648055d6d4aee01f0780f5ed89c7dc1c5137013854403c72fdefecc1b5365a92e87b6ac3950c7e32abe28268bc1d7669974ffe7be4ed36185ee9b1a2cd8abefde9c2f6669b4e2ea2c9314cb84e105472115dda349336893a2ef5b3a475218b0c6595ea1630464c704a36f916a443e8a37ed5e66ae585c5d101afb79be354b1161210993740b2fbfd76cefae424ed766a0aa506adaee8271afb8fe2c0174087c13af5df98c8167f1dfb1be4e8e31ea7faa4459cb9213bdc6898a89b36f8a8b8aa27a2b007f9131d02b4bbfe655542e3d2946a662440d6ea8ec9778afcac17c9953d274b4232ea2c38f036f5b71f390b2a57bf3f6791de9538aa0584a2afb261baf48f5c2b0583054216d1d69d3943f961dd00b47a492b9190a3034efc1b13a56e06d1422aea3d807cb143111f960a761a998a63cdf529ba70aefb8cc4fedbae14daa05c053e82a14c0a7995835a8a6678a4f5b4e83b0fd2d1d8988ac6acc3f74ae082c1b2d6fffb12b8dcb2d5ef34967623f27186c1ba69126c0b94899a70eef1553f9e62336d5e0c4557e0c2e52b3b5b1ed30a7a1a60ec3401a638960f1913db9436f5d6b563a101c5eff0d883dbc5a5ed2825208307bbbbcadfc9655d7cce2efcc1ac6cd7eba6f8b8c8b549c9400deda30646c92c1cbf464f03b4d2e3e392b13235ab02112e27ae827928d9e1a114a475da6546902a2e9264965bc20c99cef682c58a008e9156dcd25e386b8bc5ebc571c51727801e8ce6e39c5e3fc7b72a716c26de177272e6ebaae655632f2882462005240264d154db1a405b5d23bc5dcafda2f7da18f5a619999e2f52170ba38dac0c2bfef103c02a552109aa34bcc35cb6ca9a6ec549462a700e2af11f4692c10ebfbe4cbb4035cb68ffe7e62ba4ddb9450146f4d3f6ff8235c5269a7f0c50da352d840abfe3f5dc335b1b2c808bb59bd146ff4b36e152ee2cef1b13928201e808d6095ae914d033e2ded93dd54d496e5cbc63a450ba4610268ce7b2491dad18b8c8bbe6f8c8d8c174d2b941e99628f09d884dd79022579f1228748426e2e6d7d72f84db07970431378d2b731cff21fc77f6c1b1d935745b5de4c3c7874e4201988a155f7c8d95c8f0814b51e6a7505a2c3422de353946f6fdd97969390663580a6e1d0274a8c66429c065422f812d8434e92ac05dc66dba53decc0758fcaa2b333db85f7715650d4017ab8df48b70055cefe8d541abeb7bcafa4781b7a3c4da5f981d56a643386952c5e45ce13add470357041f17b043359c48ea24158a6dbed53957578446250ab47ccdaacd7d36f7c3cf6b9582eb73f8c60c8f76db1fe1016be5576191b2cf817acb686b3086f0e3c7f448c9577cda864f78916451f4e81a7e0018fa9b963b88010a2c2d58c8d8cc26f8d8e8d4fa55a014480c3390570e0683fd856e287593b8c2447e3b3b3232f041abc06eeae342a241fca14451d9a3740d7b49b50117757da924cab9f993bb35f2b541735a9a168869a50c3bf892ad00de1f198c5a5968245b73148bf1b24934789daf302a855ec87ff5dfda53e4fb2b5b2ddc3a5b0b9960d545fe6de0394ce89762779b2a12de44f999d0ac66deeb22f318d21be98c34bbff0a60d842119dd3bc51ed8198824e2e1aaecd76663934008857840f8b011a5e1f479bc41f0c63d078ff91df44afc1a782fc5973ed56da2b4891724e0944638874c100797df8c236d7da5f588db891aee0dc7227a4627639458fa1dd7501ece712ce03e644a9a6b89f140e534beac295ddc8d8e8dc96f8e8f8ea40a2fb57039e03bd41d7d0d529d21e701c2b5e27e881b6f651389996258930c35fed85c403fb93fa0ea4c05a682a485ebcdb2abd0b9a3fc9ae277ae08357836a38b5f1067a9e7ed27d498be601e86b8fcbbec51f23e526aff6d25b473aaa3bbd118cd940011f474f24bdcd6e4c54a259cab63d8bbad00d9e8fe614cf94b48278c9e96ae2024fb94e90c6454f2d1d435ba979211a8739b2a32318ac6ab4095ec46889704a192337e298e0885f115e78d7a14629103e0b88b1d384162039e7e3eb54e6a424b25c36a4ec4824528bbe113b4b2c2414bc7fcbc40d733dda097a1dbe68d5215103dc388554024bafa8f725749d32fd29362eae8ea07888614060c7fdb2e52a48de38e8f8ed06f8f908fdb794d28c8003517961a47172f90a5c238ff58545ccdadfffccb84bb1767f1a81c2c1ae5c52df04d887618cf12779322a0747862c998dcdd441b8f44c5d1b207afb570e4a4ac93e7ee8996b1ac6c55714a618f63c8755a7a8863ee912692e8281d321380833ac341b1bf147654a0ec4a8dcb3c8dd0a728bc05139120665769246d4af988f9808f4cbbb0445e976bd937a88fc0d95e6c58947090442ff38bca87a30e4e30d8dd2d10ad87ced9d3e171aa333508ed8680816d94891a85d889389c7168a166282530456445416bbaef46c0a95dcc7cbc8ece494f70819969315d1ed292d0937af95c71f75aa68d8012df7477eb244422f2ebe468eaab35e674e0cc36b05276ea7e8f908fd76f9091900dd40aa8905c73c83b85c001ecac897425fc86386cce16fa6d4d79159dc143a355dd26f140daade3432bbf3456dfda1ef21af0cb1f9eb87c041c3f6c8085b3b8974983df6aa4ed83099599d62a36ce496f9ec7330f171f7604006ce90c0fda8d8367bd21954e5b6a36a6b8160d72fa22a699d5ec21271970a3b6475b15e36478c31ffaa39151decd3eec133e05664c2de9553938dd4b88567b8cc3ceca3718349030fe02a57b8a37f96da62bbadda3f73b19f5540c35a004a8cc6771dae28cc2115c5c3cec42c88b2ab35c7244bc3f66b533d0c51c86557d055e1d473803f41e7c43fd6a5a836e028c2b661822641fddd269b6268b5d7c7ec990e3564841426479ebbaf1909190de6f9192911ec9096aae8eee7e1c0d52cfa0dc8a1d1b8650bcfb357c0cf690a3b5ebe84b4a2621b73c8fcf09f742d0af56830c40873766f447424c3e555766ba1e8fb4e4688086256e857dcc223a3109a99eb1c0fb95f349f27bd960a4fe91b9951b84d040214991fed653436e586bc1978f5e8428897b5ccbc68ac20a662d835a4fc63a759947b7c03b00432541f011be1c09c648b279eb2403437feb5c261ec57d56a721b5e9d27c315d84f46e3122653d404b74493e3afa050bfab14b5bb6e08a9ff18f25acd547e1ca55505c4589107e911cda976529303af3499df9d636b967be7a5f8a77247e69427caf982f76e8073a536c6695f215bb99139fb74ac710e5b889cbdcd891e2f8919291e56f929392344a71b5f6c73ab49b728f809ceaf53789304f61a73394a170d533c577e31ed2082af4907878832ffe3a725bf9051a214dd962be6d6339622bce07a90cb794a889496438bcc7b3e4f9759d7b205eb20886e7ad22a738e77245d4d983274ad7531ef27e8b95ce8fb3e7b4f453926f4979b0f4bdadd3b56cff2e871a9d705b16ac942c3ba5f02b0be9629ecf131247b1ce283b3d7e384b5aa92a79093240da394fa843003efdda484ddf03d9465355534dbf49f0c1fe7d6f76641fc1a05359d3239463ec74a5467163977fba3932343778834af1721c4788c2902493bc3e324ab4aa666b6f7651e9869ace73f8edfde5f2bc8df5f84113035bd112c06c95095887186c56ffff929392ec6f939493edf355798c49c6a5fd5c4ff917182f091ca8e02f4303e27c6d03077f5f3c485f0dd49e8072cac00d1531f153a46b37f1afe73d93684c4c2e1b8d451477da23ad927a3a3912ddc0c27103e2395ff7b9b73eba5871930af9c81d06620f01c18b2a3414aad85ca8a116459c1e366572c60c84dd3e1e5670a3691abbfe35f4214c2624cb01260b495a712f144704beda443733d7913e4aaa9465620a737ee2880fc2b26b308ba6da62a8bda9850e3b700ce406e560898008f8ab825491b978808164ebb30a349ea8eb65eaf8f64832aa47bb89ed736e08180deca15905311fb9521368230e1837c9509f124a031a95f7fd70edbc657bed7c14d39e2a10ba1e7431f9f15956af867ff36f949594148d456ee038143404c1ac13f84bbb70788c336b91c52ef7ae3e7f89ce5fd58a52f2e3b648d7541818a3258600b94c22c0a3532e139e8b16508de25110a020918b471fdbd0c04cd2a8f1e408331b9ab4d18ec3a0b9a4b68907b6c3bb555a0bcaca1922dca8370f636381e20f5cd9d2baa106cc45e6274415c4c473bd3137f17a9519d1f370e0c61951d001cf35138abe0b52cd1bf44764b38f6ea567e28883bc9574c53e5941ca0da0fa857d6cc683195002c476b7bb3fc3804661cc5ea67d0acd1fe22c996ca12c76568fa08249642ea298f8423d2443fdea28c2b3b307450b68a99bfda823d7a56ce9a6e8d6724ab67d40c7840a7539357a5cac59dbf0e6ab1e11a456b576a18a7fa8f76f959695e4d020600cc4a475a1c23ab1017e9a09b2a74a6ab56577ef496c92f0ef3ef4b20342b1ebdb2f3c8b7ad3daf96de3adfc9d2492c745fc1c856ed8bff40595dc64888494bb5ed9fd89d9a6442624117ea303a5389af92eb8d9406f41aef5796efb4246b50a48c019672e4f3441d2ca83adb631957ca7d7dd54a771ea84347950ec0bcd45b8e9e4a9b482d853452fa32080136ea98f07ba297d77a4fa4e15108cf282028d8c64ac82d3789bd1b29d5d4e86f184878567c35a5c543374b6838909497d33769e34e4bb667cd6195c4dcd2911b5553ef48a2ccb7fdc26dd27dcd6e5dd4ccab024c36d24317524d7f2f784706fd316f1a7cca32e96c6269c74343cf9d57b70916a917f959695fe6f969796f52fc4ec70e3929472a835ffa42332395f17bf90750944d671e7db66e7a962482d6cd5817e9b98cd9328b3ca0d5a55bc722d316090ecb4078c008dea4b78399d8e0128b275f1657fe6690a56e40cea0a471cc14bb3cf127f65a7b3060d5dfcd4a022c0b9987577a4401b93f34ad6adc895ed74bfc9242118a7eb4adf762a6158ef1826e90045d8eaf02d81d0f8f98d87e5d68e7d2fc35b329e524fd7d96494faadd71e4913bba9396906ebc1da009dc2f71fe2050e88a52ce0d756636194ff553b535557a9740463a30cc001d4edca8493d3f2f89a9bed39d443476dc4b95b82d6eec7da52b5c20984d8d662c2b2ea6b01cce731253a410fe97484980d969daad73f71cd957f9697968270979897277dbb3cd0ca876a57f17d5dd4450b2263a96344dafd6d2bcb39486116b68cf96b4809fd2c37b8487af2f022bbc58452542501238a8a6ffd653e696aaa43c7aba63fda144b442c1944fcefd831b2aa71fa60fa4dfb0540ccde8ea50b30d45cd45ba1a82d9689d781606609a73a234914b2d2e5bd33a2be9a4107e4ad435050368d933762c3325542db17c8a032767e93bf4a870dda75054441f8431cc3aaed1e86c1f2a4826c8683027016cad36a8a7fb14f67680384b8c36b4f6a419160aeb49c8c78c8bb6eddf70e98e21903eec9498a1b4318195b1f45715f5c09821f4160db6cef3514cf309ec53def89a8a936a0d40960cdfeb701f847a8c8bc05f50af57c1b22249c7f97989789709899980337cd8fbeff2167ed00d5414e98a5c6e1f2c8b2f75ecf467200da539e23ef09ba998611df368a833aa93c0fe603f4c26d2216c9a2c6c6901f8176b349572dd4b4f41493e262ae1efcbfa7fb0f74be2c259e0e5d453a4ef715a28d91fda9cb557fc27e2567af30ecc1453f2533d391c8af0e2fd7cab538d6575b29289564da7f00697ddd1a7d5be52910e73bbd06eb136715465d5114c551e50b1c85ea63ca10ab234e4761f96dd574b18767d7c5d1bd2a89bdaeea8dc97c29c0996f0f5b9ac429dfbff03a85d968aa074025e3429738b07dc9402702040fe76b7917f8cf4e64ae1165c5a6742454484197c0944e47209e268bfac899e7ac3bb33ea58b08c876ea84d3a37f9899989070999a9962aea07701614d56ef21b3f23c742b3b4df300a544e12f8d75c7cb845b3f1df035cbc85154b4e5d63eaab6e6c5fd4a0967006e9e8da4a28b97a6c76bf1a15d04b9587c701d2830332b3b2084e02d3c954633d027660f734092c3af9391a7c3b30f8ccda06d86c35ca8e1c2dea5a55252b0bb9710490cb4997c456589aa030376cc6d44fda895f56d851f4fb7d1d29f449bb134cda4a570ed4ac7ec56ad74d6efabf236a038be7d74640be56687dd54a2960ad3e524788319fc2c53e7323d0e84060a8e61483f15daba56c8d854093abfa8744612b5dcabc90436e84298e5d90744f1933db395e2b6faefac3f5c9bfaae0d73208e769af6de49273f836a636cf377da8245aa999a9997709a9b9add8f3598b71aa75a536d02b7674726f2fa97c6ef8b8b4f42a486ad9f97e24ea1c1f29aa8d3aa15ad8eabf3ff9aada51deb6c011542047fb863987f7f3d4b22baa3dc9e78f870c0cf70bc38687be844317571f3a7f1f4cc375a8becb56dcd172b8b89c42ee3550aa45b6085471e04ba60a2d6fb5d5e0de2e9a928736fe2396cd2dc8af28f8783ade5ff41d2589ffa511c6ab6fa3b2cc7ba1eab732862f1698ec8a74a0672e9fa83ebbdb2443ab11502a9266d82c34ca244af0794e93e3fc80a8d8f2b469bbd19e039507ea5cddf067cba8609b78f4b8dd478cdd59b5261750aba70111a612ccd7aa5b6266657da91e997cc80e6c631e8545309a6c881338eeecdfefe8c34ae9a9b9a9b709b9c9b8d03e1b71a5a7c37350664d2cb2583747f84d9ff0b083a1ae9a63da564cd3d66f785e3edd6b09012d50f9cd228f8909e6b3e292262e4100271610201c4330e7992b6f5a4a942fe812b3cb25051855d0b997d0aba0ff8a45e01e5b791f0b59b6646038255d73253b1f8edb2cbec03b36a91f9e1c6a4286f2af6228282a26c05513484273209d86b69515fb5e19db547bb347ed63dc059739bb43c19347cd02a2bafd1f9f008ba84b57d2365350a33de671eafaf87b80f34d61f70e5d4a216c7c59476e101b0299f2a987ef579ded6bd87932d799a315acaf93cb682b1a9017edf676d20fe7ed4e6f0ee96672ac88e406eef819aff2a289fc93f7585a12bcbb9d1fe44a9d9b57f9b9c9ba270a99c9d9cb3794133eaf9b90aa7c745b18fef0b6044bf8fc63143e9e2189f862470d6fa15356a8d18e9aed5f56d6e80578d1f7b4a1b3b35b76d293900419997726a7c6c473693347a6ca73a467b601198efcfec781c4e0e8f1bbaca2a06be7a59f4a11aa3574ca4db7d256a0cf1886526a4674849e2a9c52d0e8c07f9d90674620f9936e7ec9fd84b274168962d6d85cc03ad4b40c579432aca3008b2e3e21f4b23f685e14e84da7c00eeddd35bfeb4dcc4b24829515efbfb8d1e6fb404548a7c8c8e28ba47f6e49c00afecdd881c9ccd19bdc3f2ab92c30349854c841e8ef7d3d3fd0f70675bb75c71c7c5b68ba0e14fcc4d44668e75e2076b2cb7e3873859bab805aae1b2cd3965b9bc7f9c9d9ca970a89d128fe59e9d53664a7bcb896db9416d12a2efd3c5090a02af16787e2583149f89b917b2f57310cc4c3f7ffb32507596836d0c91848b21c9e66bfd84900a0f4dc524de8345b6b7673c38dd9d494282c9eb32ffdd4a30c1c176b72906efdc7a08c21a8d4f439872e8dd0a3cb927858a985ba4304d7b4ea1af96c2b07f4f11a1bf4f1f8182099019724d225a3345c16a1ff2d68fac2633db835569eb042dfdd55250d576c803dfaf7e6fa75cec29571bb9f4169463523b0c0c5088de6ba35077f5c7937bbedac3d08aaaa2f512bc20aadda84de8bf457eba00dac17b2eecd4b4fd464a658921fddb2f3fff8fe07d7b26fadaaaf0482b922d17eba981e1875816a92c32e0caae90a25fb8cac39d9e9db0709e9f9e9859cb5355ce6549e52ba76aa3a93c8be40a573cdfda63619edb1a804534f43c95f840811755e45afa16e016bd4f80c70a3a3ba76d717a4e84bb8027a986630b8e40d54134116a0ef972866f577dff646a47d999b8c5f1ca8963e8aeb4caeda946b6f8c16893d086fa90b625dbf3dd1fb8b1df024a71f21ce2855b20647c6a016063d6cdd164da5e00d1af99927a921ba4e96b0d3cbaeccaba2f56a488ed241d94fa662dd4c4408321f491ae907fd1c24d6308402b922b989517819ccf05cf70434c8f9c2f2f02f5529bc375a822f497858d1778f224743fd55ede945b6e37314df922c97544f3f939dff61ae20a7a45f397b51e85b8e55da6eb29365f22a022bd6397b1ca7fb770a99fa09f1d95af97ce25d26f5a2e268172e1779fcf4fc016f3245edd5baca172ca022f41fe67a717b75a6a402a24a3ef06330ad6812f1efb85c00166555ffa95a664f9c9a92ef8a461230648a30167b3d63eb2dcf0b46c3474a73fed7403145041926d58286068d95ddd0c21ddfc9c02f40494939367c6008cae2b9ed9f26b102a7adb9b807dad19ccec73a4d591c6c5cde5b7c1da9c12a8acc4b315acc8f7a74fdfb38ea74413139392ce39526e8869dfd67db2aa685201cffeef660cf82571e5cded036b5c4089f92b3cb9ee83ff43a87dcdfa90ff41199880935a048e7577a64da10bd99113eb69efca2a0bd04a9ab44af55fc896393ae9c30542813638c07715ce6845ab6ed1ce7f9fa09fbb70a8a0a1a062469348a94f75248ab9ca808036348269757ba3a5779792c7156606432c80741e54257ec464089ee8ba2682acb6c10c38019f51357ebfeb0d078ca2ec89fc2cb4afcd6b4e9efd1aba1911ebca510dd87b3161ba24c21199a87319668f5c672a7e55c1ed7e7613d610829c3d9ed9024c81200aa07620abd2420508558ce9e46c872d2ca41be8ada6e0c45830fbdb31118678cd0277a5d8730ee30be9b2a78d6c88ef619a0e3ee816e0da9bba18f5f2c8be418ea3762a8d3b5d6e1b67ae71e7726c4e5c1a6a901c3729ce4562a9241e5f8f478ba5af0d138c50a36b1a19e607f0fdb864fe1f17aeff3d3b6bed739f3368943f99b16ffd224d8df3805380b755def8319d9ad5a0a1a0c270a1a2a1eaba8fa79564c8bd5052e954ae7bf6e0dcac40f783ac60f3c7840f064bcdad38a258c421b726fa36d0568f7cbbfdbce7cef2a819a3b1acde476e957c08702de194d2917b70f4a04536c6754e1a558cc8c44ea7091fac76e6dabf81a44f40da73c4a4c454b70aae52071b2e86c590f0e6a82a1849194549110a196976102aaa44711af163dc6b2c2e208768eab7a357b93900801de6928e1e41f123c1bf25bf9c820d5c041df1477a1c24f9e841c359c71d35e6dca9650ab6436156810ef2f0fd40c675723a2499e043014e5ed681faed90cb17e7800f88dbc44293d9668a8cc2e2f3cde242473cdd5ef8bf7d8fb779d6af2ee90b657a20354ec6efbd17d742cb177eadb57fa1a2a1c970a2a3a260edf45d755ed90ce4f07a9cb7465098eac8a8d128ff609419558ada91768e025bccab25f52d90d27486d8021adc4e199ec64b3b4ea04c64536445a07dfabf0ab143f80e092210085a151b9b288e7922eb27ce5750b806a32114d81e804cd43d3038a0d5f73132f9c7b0289e81b77898a25fa3a8dd5c9b19c3767b3fb2a1da80e8adf7094854b3e7e8e5d2438fd1f7e8335effdb229f87c984e2661176fbf1588011f5545aae7d6bc059945fef1ebc5af3cb0adcf196c72bfdbd8298239b164acb0263d4d3a92a2be7ba332128174011aa0be13dba8398096e77963e170ad4db4ada869cc515e6ea8aaea5c941b6c492b91ae1ea724da25aa2d08514ed4375d6fdf011c6e37fa2a3a2d070a3a4a31992ebdb77c42f3f50b364a7b9c467d14e22a267d4ad7043c2bea0addc931583fea8128231808220fcf3a207120e5faaa2d90112200124802748280003087a8a608000a302c109155309150f0112263abbe85a5be171d01bb82bc817bb6b6d87c9242dd1d945214243fc079c0c6e111cad5f4b863adf97dfd90505bed77863a8adfec19fe06260896f11b65f0b05139b0f4a0ce4c8cf2f6ca88d863fcfd395049aff85f36e2346812c843cc95e2b8e9bae0eba0d9e7c138e85607a70d7115db85e4b83160222b7b30b1c24aa3100923f5ba4c0591840dc2ccd5e2b8e9b8d824bfd0391f4fcc2e7dae9cc018f1b15772cc844e8b4ef13e889831c5fd752031264671736f44683e665d212f807ae122a52bb560ef14f3bafb30b114e5f1d03aa6741a17a0b59d08853d6ae9541cfefc7efec4281df6b04806fe0ae402ebfa22777ad3866d69595ce2e648c9dcecff9ce3e77a3710a2e092fd8cc4fee5a3e481cc0b89d5db0903117705078cf0a21332a059ae0151754d95dab0e9512caa7b30b2202fc0603eecfade0ea0659d088ca97ddb52aec90f17a3bbbe021312f2e0e7679fa912105ce4230f12e5beb5ada51ddb16093828b77c0696b98fa5733c6d368a4057f2271ce784ae7aef5e3c02b7addc47121981edc3a460f6ed76aa08380f0edec8285846a04c06f233392029ba084cc6cd15deb8e83afd5d7d945014faf1e03fa67164465240b1a515d9959a2bbd6c783b7a8bfb30b0dbeaf71fca0d21614ae8cae8246a85b36efd15dcbc7815794dfd90505bef3f107e91db151999114d884aef815c3ee5a75a894503e9d5d1011a0ea3e065ca0e2ee51169c8970c7f7bda6bb163d3192af80ecece2d6ab5e2e415fdf8a04331a1de403babb4d606ad772504384e8edec8287446b0440f5cee2ca8c4a8126282b33b3e8aeb5e3e0dbf2ebec42c0d353c780fe735608955159d0087597f3e14577ad8f036ff17513c785607a70d7111db85d6b831602a2b7b38b1c26aa3100faf75624995129d00465cbe63dba6bd971f0b5f23abb20c0e97fee7f810e01a5c18d0471dde6b5b86043ca97ececa209d581701c6c08f68f8c1438160215dfbf66af258e99b58ad2d9051962d7b90187c18d0410abd782b143c6ebedec8287446b0440e5bd155c8e530a648262607bad75944ca88b98382b0653805bc7e8c1f55a0d741010be9d5db090301738b8dfbbc991194981262445f55a39c4bf2dbfcd2eb4a4b1d3791b30b6d2dce3ef7792f0ee339ed8d96b7d3c788bfa3bbbd0e0fb1a0150bd5f016546a540137c85edb5caa1bc636d62e299189402dc3aa267aed76ad04284f0edec82c3c4b8c041ffde0a2bdf520a34a115d56bc921feb5f23abb20c0e97f7ec067702f015d6baf35c387dd97dad9850db55ff40b306c891e0d182d282590850bf9159afcb5e2b0a956563abb1023ece7be5cd012d037b84528d1da6bc9d0e7fbf03bbba8e0f7b246ad108381fcb59cca4806b440aab8563cfed72a506246f2e5ec827242e84341ffcc22aa8c64c146a821331bb1af850f7baf08bfb38b0a7eafb145b6220e96a49f513cc841e8babb42fd5a2e8ac821bd9d5dd01991c707826952fd3f523a381fa19a3a815bbf56821f72a26a67173e745363078d818b8210ead78243e584c6e9ec828810bf81019fc15d825cebd79a7143ef59edecc2c3ee2ebec241eba04f702b109dea68ceb2b085347849cf2f6ccefe35580c5b4a05c3afa0e3dc100fcc07f428f5b574d00d60610b49c0929d5d98d9dd2bd082b1654b34c605cfa3775c66a17e2d07354488dece2e7848b4c68b1ce8385824b195fc388b077300dd52bf560a33f6146a67176ae645d9e4776630a3e8603ea0d18d10a85f8b03150222b7b30b1c24aa3100be817b0578f91593fcb5e2b0a956563abb1023ece7befc8a12d037b84568b482ad19ea7c5f7f671715fcce47fbc137f6fc3da4c05918ecf83e3583ad3866967594ce2ee488fdf92dbfa204f4852ba355d088ea96471c635f8b0f765fb17e67170abc5ea347ba421c24727e507c50065d6d997f0bb65c103143713bbba032463509120b315091ef7271371a1de4030add191b4f366ca9855cd2f30b993556014ec296d2c1f84c38eedf100f7290a067e125db6bd10b930216b6d4802fd9d985995d548759c556041d402ee858101f5075999ffab51cd03020713bbb6021632e08bc810e815ffe0b3ac23a4396ef3f77ef5abf90fffeefb5b82425028cdd0335d7830e48c340b42319510f9f97430727ab2e0972752ea82c0f3c1029c4ec64b0b5e01c09a54fe2402ec726b37fb5168b38a5aabc6763e003b1c1c9d033cee3ff90ce67c1b62d370e1d1bb6f8dcad2aa9df85fc395cf5d1ba13a6e66883a7647d76c9d349c8abd00b0693461d8fab6900eb5ee558b0f17caf744037c6bb958983348f034ac44eadf6b40aaac3ba58da13d8e4010819e50f2b5148c76e988c3b0e99047ff91bdc5ef5da48f3f475ab678fe7600b0e691651fd5bec3ab3dde61a12ccaa17c729127a8012b69f97be30d5a88ad4b7832a735f5cd30a10253055323b23f6dfeaa3a4a3d770a6a4293418a5a4454e495d0970f504d791532849960d3e90ed8d760e3d299df36989645c0f187c353a337c21b74819dc9290f0ae2196a75e01d429b7166884cab450c0cab4dfb98928f592a4b186c81a7d232837bffb8b29cef8ae9a3d161479d5def72e06a84cbf8d3fa86a6c853643841b280c2238ba75e7e1a13f954c47d22defc7dc6e0559f5cae88164663edba78f9c2202670142807a4550d8824cf5977ab4be839fea9aa608d5ae698bdac07829f22f2b9f0e71f909e53a5fbc009e8dbc48799d22dea43650389d4c9729f295512e6cf965289f5bb4b78023b9ce51e008480571cdc0d326e8eb8143b91f84eab56cdd2ecf61d7e9dfd95fbe93889b2692fd089e260880b31cd5068e177404d815d0726db0f51a010020eea6db70a9a5a6a599e4790796133481829c0ed643cb2187a8ab3abf23a1385de7a8f8644035c8829697acdaba28fbe975eb0c5621941b547e715826bda6a8ced6cf7da966ec37b78e8bcbef3580bf85d6b2aa0a5d428ccf40959c2d5fc9221e8fe062143370e58baaaf13927fc07a3c2ef6888a35463ca0b93775bd24e281a14e409831fba42e2b596c0567d768b90c517d48d227b8e09793f2f2931ee7e69d6bc064ce62f5b29f53345c98ae0bc42aa1c4c5eb14e9c3b933b66ee7e002515bc581fcf5bd0261deea875a06abd41ef928c384542a77b57a83da6db63490668aa7933a05ffa5cb88d85867d5dfe78feadc76d65bf00553d38324b54c943100933cf007845c721bcca42af5a5a6a5e270a8a6a7a6ad5f39a9f8d95ba4bceef55a9ca753bb797dbe847a8e80ea784139ac28d9833c3336b90a077a810c8c7004069fdb5b2c6448e38390ad324c6fd114bb4f3bff49983691b3237f6e942f0feff94084875b22c2025c30d7c89b05d9866a6ddf8b01acc39dd9cf91d64c9a48633e205d22a02cca93965d8074f5f1318f8b8c64e35fc8cd662d29c85e054d569b99ade8e2ba1968a0b87d305e613673e997ee07db08fda685195edfe3942cc269837ebc4d294f776d0e7c12ba805631750ad81e225df8b018c4b492809b71539a5ce41262c2a464a6c31a5ffb15543c6ad73f67261cec1aaa5dc34bdc94cb6bbc273961435b56ed26c3b0295fb7ccf0bc946f1676f97625c99bdac2fda6a7a6ea70a9a7a8a7e6ba944b1863071484d4c16d663df15f52a020dd981df6676cfd40d37bb5c10bab4d6252d01d59f5d650b738dde5a22c0123d6671a26953e2e4b617ec7253e26aafb94d8cd0ab067641432baf22f92797f7b2822428e9c6ce15e6fd32339e2d2c236c9fa8c35fa197519bed90d32074291ea7cd8fb07433bc4cfaa7e11fdcd5a37e0ea20861eb9e855b3b26d77560003b6693bc03b7dd4229ba4837e3eee87c557f1001903e88ed342e13dff4bd5d08e2fbfd08824a472e22d93dbb835b92c4bff09aab3fe8c0d236a1cd72fff1b4bb340b95f0f415dd913b570e93418682f7aec3c5de08fca4e9872cc4c1d89d429b7662cdbf69bac7c6342c0ab80e947895717a73f84a7a8a7f1a8a9a8f16693295a1dba3bcc4f4412a8d38238e25d9ea81ce2df9632605da2ebf2b6f111530c70a2d3ca2b19b9c3b0d7e480c11281430e682295db516530572146a3b3b298dc97136c530377a2d9ffef14c23b3433bf3101c55b525b59135ad881497af5e166114f06eaf4fd6c627ecd188e96b4578e6a5a926db6d3b6e88e5d185ba7a0fc9f5b55d17f08d6a4f3efabdd6c69f8f736bc61081efce1498fe47e8bd49ba8492274577b3a37492bb0836a45914140230132459b679f4f980437efd4da1a35637bb2950adffee57038c6b7e2f89aacf211cc6628b6ad1f979d16156898a101ef74f6280e8f3001dffa86204892aebc12efd92ca55ba0f3bf5a5d93fbaeaa7d9b490c8ba8a9a8f8a9aaa908b190b74a1947f918d22c1327c4ed59e7f34a226a75b00efa78f04ed2a9549439d8b960ebf2a04590059da0bd3b9e19418a7055e514a8b5b00338bebfd69a3f8051c6661de67800a9c26137650002bf870aee90f2f60186264e9123a72bab09ec951edb33bee87c9d59a246b1dbfc2bb41bca85486dd5c71f5c4c83becdfa443b0e44bced347849496718b3103b58a8c266b3b9c8277401aa9439d03a2c45d090813b48593a96ed8f95902351e65d02f1c404f1f26a1498d34922abb2127b0643da5b85e4891895250e27265b25101ea06b6f621bb27c90fc77a63303d545f95cd57fd3672e9790ec910f3227844ac23ba229f14eee8c87b7ea429ef0da763457d7a1be8fa9aaa9fcaaabaa1bda5335ac521b72a9f0f75d116eaf8c372fa8cacda0c10b451d8e14b152d7368c906cac9232298334994512018cd98518c413ac08c1ba0ede89d71d9c328b96a73e43b36739e023450c2acfa610539cfb5f5bf4c73ed5576457075df8463f08375cefeaec3e44c0ecd7dde69cedf02eac2eab8b4bf42527d552201d135f794d7905b4d222aa5d755d3c5ac8e89258f126c681e0e3d64b37d1d9dd425d0ab757926122f70957943be83df0938058eacd197217c0732db7074901a42f7b542ef71a1f65a211fc68d299fde87acff84243aaf14fdd14d0e561fb8ebca32462797aea359975e83d276aaae3ae2d7a1e2b77fd17017ed41d51ae43eebb492cb12acbe32aab6a96aaabaa8371a9abacab49b3dd9c8790426384ec3dd1714dd927e76417f710261a9c18be86a13670aa41853a054598063b2306866d137c6cb52c4506db4065d1c19514f46cb718b0c462b8a891b8c82f8b496fb9ca4562c59bf987ed719a858f0b9aeee84f662fce189eef91c57124cf20c72afc29e42a54afa7b5bd037718f244beb3df97056895dd22bcb5b2b79b724ac3a642d3483f3114584478c1bca265ad7f3b5c006462ddc8b7b46120fe2b345ed671200cff55dc647c6eb5965d04a144b67b3101d4cf3ce73b734ea8061b498d64d604741d78120f5ca26c57563b0a803cc5c63cdb2d6642f32cbfb708c1900dcdd65a647563f15090c5b3c02957c844e76243433d242b78ac776f33b89dabacab8aacadac212671f25759a36c11845b7946b9486ed7900c1c2e00c463243888f79bddb9768de8a6f12ffdf527b3cdda4e3ca6f32a35bb6e1922ba7b9b58c4b4c5f062f94cb103c6e3c99adf304b8af2488556e3fdf2504a1ea815777550ac06ada615f28d895a18dc5172c2491236182ab9edeb58b2046128dc999ed1d9c283b0339efdaa362e2ff89a9244241e16dcf42846a549259da6884b609f7e808e69718e4c2d62b868a39ee4aad008aa3f8d4a4953f579d606374d7261b160759f023a9d4aa0000572086c41ace8912cd788cad5034540916e322f1832b4444db2af8d0923be8e008102aeb7ee4a74e299c98097413f804b3eaa7e21c018c7b8b112b309413ddbf2cf1f02a4acadac91adaeadabea1671ff37d197b933d9465ff2d31fb637c6cc7579f8a1902c52f59e482a935ccfb0c01e38ce13e52fab86862ee2551bcaa56acf5fed6eaa4da8f1fc93463c97c232eccdc5be1125c409c9fc98a55dd6be531c8c86e056e13a0a05b9c40b59b14def715f9f45ecb808b517b731dd7193ee1ded4d2bb798c81be0a39cb7c91178799a528be3b44591ace5601bce79b5f252203d274ead86ced6411a5ac2cb36a3d0181343bf3587f454522fa51a9c52f9ebdfe7411eb27550408196607d41a03488c9cd5ecbf69a88143d5bae4436b9b34e03ecd2001cc0bdcd0e9e067bd6978b497853c6b7e24cd834218dec84a7517cbec60e63f7148f29f50c38e99a74e4f9035797abadaead98aeafae6f4c1f12a6c4a8f8ed832e86e77d6f919029baad6b4c3a683e29de5be4cabd0f316c20616be9f7d5a05f7d820cdee220dd4496775caba7be7896bc18ffe0f266a191b3be881890ea0e6f889f8fc931dedebb93edd5348a616bd77c9c665b9639430cab0212e8a49a5a9d2e8238fcc2c185e22705775b464b9a42ea4992baf378c43b802e0cf6c9392fd32e74d5493b5f65b07bb23b16512e28e53d1a116dcfa0b83238467d47718fb947941677e3913570fb049d6f2c50b6b7702f68bca2206dad5298e87c27e66dad7d3f36384a3666a5576b1bdb5107c8525a671d396b309a8d96a9549eaf4e00a9e5650794eb01734f62ecc778b35e333cca56ca1e29c1688a604b70b2aeafae9fafb0af54d0cbaa7938d39ef29d525044d9d6e1b0b57f53ac848328a65ee5da6aadbe997bbf80b6cb8dda4de9b269a6f38f9e1e01808a34b741913c5f4ff86419231fe08193a9a8eb71fb969866294765450b8c72cdbb240874aef429b829b7b146b3d0dd0c01887254ebd2f61b3947798e8655adc303498b087bc951bfea70967f24979de786b637dae1071fcbc093d4a0893379fae4988a5f866515ec942da33fd492a91373cec422bae4fc7e57365c4a0437b684d2d6141343d2642a55fad40ad2d92cc3e021799692ac167cf6b07250f48d87dd5e33bd2b645db19e88525f7e1d7af33b7ba8c5a0d05ba914fdf27bcb08218858b12c0944ca9c2e815ccea26121308330e559b9afb0afa6b0b1b0fbc72e779978afdd55ca32a1f014ba82d3195cc574538f9f660630e47588a94be2dae57ea2a4e7e0608442a2daee5fbfc83d9a0c3d3a0da985ddc9cf78bc8c9d84395b2b0795c86bc2dffb7ea303912399015019708a3acef5689521d4b3381d626e339f9a1bcf7343ba6f3382c58ebeac8bf57b1aab3ad1e0122cad528cf60ae1808056a6bad6ee5aaf0d99df77d388b61c39914ba86331aea6d7bb709832bf919eca41bfa78900ee6ec9ebd823a78ce0f0381fe4733b910514779ee018b8c4d9dfceb94e5ac57970489c94e01ac18b840d769c01a65e7bf305b07ffe559b2b64df6b8904ad8de1f276595f3ad38a4936bf1b9ae108fbbaf26a85756c6cc5c19ee7a1eabdb0b1b0aab1b2b18a3a4329b30f1379ac5cc1553105633ed67a10f2a04b818dd87e585b903c1a180955a840e4e189e719d0ed1603475f2046990d372247c50ebd2d08882dba86518a0f1bfff3ea2431e5870350d375a643d67b68cf14b9c4176e493cf7af9811e807930395fd4b4fc294629810f206ae2a8cf609a2e03dfa9aad979e9cbed213ef574a04d4867e89234abc30017e0c82678701bd57c7230d5b3d41e358f18c1cdd80fc144336158b4be3e9202a79f6ce46a5d5806a04139eb2e5907a3b6d2f39b562f1c7e2f139277fa37b05e4e8fc4c0381925edc5cd0e92e13c7799277d4639c0253536eb1d6ffa2e57e74453f6271b0db1d4ad810bd95ba714f0d43d637ed17ced13f13c4b1b2b1b1b2b3b20603b2aeea781d4acd61e5359dd108c07f5510ca7d54293eee39cfc87341c6be38de18338c06877965f7d7617d7ab67ba16a266292d44c87327251dc96a12ea4b6682500daaa17f93ee5cfffb53ba4daad3ff6d74364283e2f45e3b0880d6ae61da4821f117276508475ff36a1131e6fb4d16fbeb7d55265edc0567d411d549b06096407feb80b170d126f83101384b22d061e8b4b94d10477fbc5666084ad558aadcef6c6730c61eb461d3ffb4a8441d7b4e9b988af2949021a791e1e97927f7643431c1a473cbfd747d200a987c344812e06a6b5e1523b1c7f5b23ce471a1835f1bdffb1cf8fbc997efd18ff1b6a463bc9e24e96dec7fc65aba99bee34487bcd025fd2c8b2b3b2b5b3b4b3cc9b4d7ba6aa2a90984d272547ec461d6bc8c00c919302f9f7c95e57750fcf8da4ba81959d277a2569b7b8b950767db993b702169e4ca528cbde2e5d50c1669b958acac692de652284390ad4a8d3e2ec001b7a50db4c84eea186aed4c4642c5754be6bb021601a5f1b75246ea2669f898c95b8eb6f1a12825d4a3f1d9bb668d4478311b3174ad78382b4967b4981400289b0467edb502c7b6958f9adf5346fecae7e0e6b878ad49ed57966960440b33a85a2d59c9823db1bcde109b60685c468f4b31bbb8f9aa782271a341b65766b76b4e1a248c0abc70b9e9a29d9679b9c55efe3e9158c8aa8086279e4f5a64cddfdbdc56a5d9c805ce7799a73797c39c73c225c28d0b3b4b3bdb4b5b46b344216c981f47439cb3d4b4a785d4d667d76d2fbb27a7732b262218cf21572440c47b87ab2fb0c81cca3aba4e6da35166ef39defac4b83c92903e0ac5cb521b9d55a2ceb5d4d901072853cf929a1a2e1da682f09ff70387561cf42b1c7eaaa9df53c5577cd415d2f210ded0edfc88f84bc3281259c4c87ebd7e7a4a560d12fdec0e6f38a65df5d48e61ae50b00b86d1a12f364766952ecc728f5e81277de6f99448f6b62b93384a371e7780e4d12516616d7d09587ba0b3b2269229574a0be95a1e2f9d38f2fb429bbe3e2b1ab663a8c18fa692cf693d065d1ff0a4c28459fd0a9d317591cd8fa66e657e456d07f9aa7f4d594385b6765d23a818e71916d30ab1e11a4a1429cd7b4b5b4c4b5b6b59e33a657088fc6f162c236ee1d8fc4782c9fc1b65baf4a400624f766369caad3fb1aaec922ebbea0b2362457daa6c00a4a34e211ef8f087a4f7133fbe535455a948e058fbea96ef8be430849bf8b9d6247ca5741bd81552319a7053ac83251552f629fe2a01430b0a932a51fa827c18a888ea0d2cc67ca184db44b1aad6215b10add141af5a01b56307d707c5bc6a8ad7413010d10e242e6c596d43ca8eb8a55a425f1c90455db46517a158a10749b1d1dbc0b6d673360212e7becaeb385142b991bc2d2a3fd593813762150c857a4f8add72cce004561c6b8984a5192ff02032a87c161bbda09ab93c6b0602ef92fb91f0058ded78695bbfc85751a524c8807217bbc72deb5b6b5cbb6b7b60b11ddfc1d324ee830f27648166d1e52c5868096f43f840f7bd39a0be7346a11362a1b3231ee0fab58b60f4ea8e19d380e92b52af3bb73eeab1386e6b3ec3e8c93afa91cadb938ed7f0f3d2bb5ef3c61436a65a95fb29b9eece33efd95894786009270713221f8d47d984a1536980b6414959b6f35bca889a9b4c151f2d58eba510934cc3fabb2d8bba5f5abac94fcc335448017e589408a047e4037ecbe49e5f7900d167fc09b11e412dbffd2d649fec3a33d0e0d4df696eba71bebe3767c82c422176f134ec5449a6a92956e146884818b1ddbbf9716947e8b625e129630d19dcf528c7ec143fc9811626a4d2df16fecb1a619a777672aa047901af134858335a237a1e4e5b6b7b6d2b7b8b7a4bdb5e4bfca4cc40bf0f95188629b4171f15bef7d80177474f182e4abbf1ed8aecdee22ac3555a1f3d4fd7bf35a08a5f79b778339502e3def3cc933a59f2aeea92fe52408067a3fa7abad067f272a53e8dc3b8998cef3d92146708e1b2d8aace56bb77c07c1d020b2414c80be01ffffa87912c613d0413323b8bd6daf9398497addad292a43c85cd126a45e250e1bad206225f42f857767e249e0aa03ec21b6944cefb71e3bc14f84ae275fd931a19f7ffcd254bb7c2323f47b2b74a37e601242b16e1ac38d145fcb10891bab75aefda257a16cff92e90b3339969aba76c51f2ce2872b82cc2413cde463dab46604fcc1f599e27028af3d13dd84636ccdf26e011cbb20e9b7b8b7d6b8b9b8f892234f65bd8848cff65a3791159149a0eaafdd2c1425e0a2fade248a4d1ee1d259647c4bfa857009b56e825025152043fc891d3ce1fc6959171b97ac235d658abdfb174fa45cb5ceaffa4a97bce9e3ebc0864590993f06b9799a0d0c1352c42d429f69920adc8ea03b91f6e630cf2db508604d86d2b9b6f0dac1a8a79c06c2852f1ed7dbf086acfa2e9bf8bea1d9049b39c93e425d2af025b01a4f6b0b17338edffd9daaf1739016b77b44d7a08d1103fbb29459a075cb510adb8ffc1eb576435b08cd97fcbd35908388ce7e2e6e898d2464b64a3373bbc04e37ac56ae94310df111f1c97965e1efe162f91432dd63befd12eeb97eed4b59fdd98edeb6a032f3f31033edb8b9b8daaab9bab98e61e99339c9576f65a49331efc76c7befb4fc726bfe5862598563748d881eaa36452b097bada506bc67caacaf4aab8ff210b890397bb7187102db7b31366cd96bb9540d1f14b87b116924ff168ae25df6c614f42d87d267a59896524e80e2c402984c1a0cefc98ee7dece26ee9ed9b3328748c5bb1a1f7842c9f895a66730750a022defd238cc85f278e6be050d52952c90e3b6bfffd8af6ca22321b3bb6b0790a53221c40fddc2dda0e5f0f3ec115195855c2c5d1a57ce6c61855c4c9a67bd9941f017c6f4dd1316883f259e9f0ee8e9ad7bd8c73d943bd9e02155e036fa7a90d613453813246ca8ac46f8982d3fcf5647aae7f1d1d285e7ad9254e538a53a513af2730ff1a9debabbbaab55b37a91e3a2ac5549c48fb86e1260887e0e321d2eb81bbed63707eaea26cf8bddfca396d07d8023e93434b64bb7a50eccd88948e068b8ef64d2d9d96f9e91b2943c33e08d3adf83123b7aaa56d499244f0f297591f455d1bd9c92a14a54a7658aa05b366e9c37ce811e269d1dcb9280e8271787f932c2690610d8d4fd1df01787d9daab7b20453408c7490d482ccd372fb30214362097929fc461ea3ea4e3810c41903181cad5268fa0e3ea91339c65500ae2a3a50d43e33048fbb01b72c00cc446f0880bba1cd2adaf58ec9ec5bba95cabe2033edfc246312e37168fd607025870c523771922bc801210ef957164ab14fdcfa76ffe426b342338aa8108be4ffb1294f5babbbae2bbbcbb6b5e55dc25f5117a6807ace7e01d58f1e72c9226cef67e6b35354490a95deb6ce20b0962be000f856a27298b252828779be2f5eacf12f91827022bc8b8e315fb8cf9ce055895013157e55b3d54d25250a0ad9aa148bac266ede59c93f1d2894deeedfd982229c31557575e2e57aad81991161b00e5214a40821a7eb158036e5afffeb66b24faa8c9acf79db4240623d0b029e8b50dc47981c3d0f6081e23aabbb61074d3712e07454a8f9da559e53c38425db4398bba8c4352162e0eb3de141267f32c25aac6ddf77985b9ca102f6bd0b93c8020705130bcc3e42458a15c173841bdb5f32a7b40da242d8574e4c82a63ac72d02f76b5bdc08d39d6e1a7ef3eabe4939429f9bbbcbbe6bcbdbcd7e1b422368de85a3ffe6bcb402f4a835f7a395fc33788d7bf6120bd67bdab916f82135ec1a165b8f01ed9c46244015e4dbd10174ff1a7c1f6d760ffff974836a50ba0f96af80d5954d4015e8dec4f3839f756d962d888c2df90530131d1c72ef5a6c964e327e3f0bdad0fc20097c7ec96cba652ca3214c61a0d05decb57a495ffaf2464a05352c4c07df31cef3bd3431ad88d3e0a1e01d2c68b55408fbd3ec9b1175406b8f5d4bb084ef9f2521f8bf478d49265c67701fd31bc185a30dbcf9e31c23352508bc98a2144293d54f4a803903404c78e347babba24f7eb132aba4c93f34a0bc5d050392e9ef739063c442ea3b5ca3227f6b5513be105328a515e7e5efca5fda8eabdbebd5de0e742e55a882eac4c25db6d96a1c9fc9b37c1732767ef8733ac3176a29e6faf8328188890a4ef6edbfeb4938bff23d37f7530ff74d6b250c1454d34ae5780a25b32b223debdcf9df5f0878d8a89912e3fb76291f77eda329ee03b6345fd66529d98b2bac8a5d4341787c9a1e1cfdd96188d2f4be63ef5efb70efaacd528fa827d311c44d4ff519040994f797615fc1e6fc4cb3fbd94342b34d97309c6bdb3b2324e8d9a4bfb4b1973cfa259bdceac02148f2b46a78364c12393d47b67132f38e1990ee2f940d77ee001d0b29cd1d68089de2525ec48ed35ce30a307b07477b29e84f2e7bc793f6a893dd266d089de47c1b7459d08cb512dae54344c6ab898ca2b24e98181a9eebebfbea75f85c2b44300ebfecf79b967f89dd10e2b54fbe20c01cf7ce5ce9c5a4812633132f308e90dd145ec82db9466b9cdfba5d9d4150d83cc2ccf52656ca6f92376c27993b795d0974a64e64d7e2d756eb30fa41e9447da271ebb20a6797e66a71706ff65776c4db958c8ac63d9d126ad9451bd8a11dea4099978424f9392a1c9786edae35354d90ab709407ebdaf1e42472674b428b3c9d96cddd420aa8a359f7f3cc390e948cd4dd532537c778e7369f5b7f62eb4058208a5efba42a04e91bf404821d22357b1e8f1217eed9c051ce11ab32ca7f3ae32395af6ce882d5e9e042f773998b531bc8c4ccab4a3b0f77d1bfc5e82e0285c2d6a8e842235528589fb1fdab5e34abb858581bebfbef2bfc0bf27425c2392f4b4ee62dd25088a1d8057627c0fab7ec2fc1aba8c05cd6a5ae950334f7d9605bd1c2303595df1e0280caf17b6bae105f7bfbf8205372a0c9b5fe49ea4aeacd4d3e7e3d5ffc4331b7834cdff4a8ce9f73cbbb9e4847f51ac6b8d58bf82889ee73708cc2f703a5fbe352132378fb963136b2ff91153f6e5a81a66d4db0d5a807ddda5e611bc4582bcf2796e816b9b5532c6aa5dba9fd674b3591e0ff3b713e8dd2e30e64edd716a6d88a5cf5611a9a3fd71322510b365a101d24926ebeb359e4221c068c128769e2be6de67828e07a51bd06e23639ad0549ef090f8207ce19549ecfefc79dc89cb906aa745e2010af0d489f408df8cd0bc0cb3ee93ca9b5a715189bfc0bff6c0c1c0b8b6bf01fec03661f63d162f1dd33dcee3222d842e9a7f7c280f1478c4b5505835f525d550e1bc8a30e2b27e2327978e2dc9d546932f10d8a85e3d147c5f9901418c58b46be0d0764d1836ba75d5437d272beb0e50d16ba885b78f949b1e93469002826b5976dba64eb47e97c3a33bc1f599305438f83cdc339b67fa6b2ad0dd8890fd472c902fb6d2bccbea2272fc49e7aac157cd75300f631df3a84d935380fdb2bfc9b49167fc799b9c9fa3efb705a67020eb38556f4f1ce16b6f8c85fe5855909757aacd3a8befd722f563330363ac8338457562be8665dc51b07cf05fa79a27d977fffb2a9d971c2b262fa484dccf0332824908bef920fd6d06455b8fe7dc5d7c52878d81c0c1c0fac1c2c1d4b5bae508940c84a0f5361e8974fef7bbb7e0285f7c32ae293a33aa5f845bcbff443fe5b1ec5b8efaf0b47aaf54170fe955a4251d7dc0ce7096f07262234d1d8a36b7ec8a9fea51cff4117f71b388c98f4f5e843364cd2ebe2077eafa2edda751ac60e247bc5b0c306f0781654eeab0907bcbbee1ce7330e7e13469f88733064eebf1fd13af646953e68defab5cd330694b13b4c7c0b165f0aac84a5e989724a94d2b84bcecee0179fb2b8aa1eae00153bec95354aa8bb4d864de521949fff114c60d05a7c7c20a90b72ba88ef28144aafeb3f24b04ad129a7201c21ae81edbe73de6d31cac54c03ce86afa9ea5f0e80d191a874b2737d5e3e7cab333edaad9814175a291c1c2c1fec2c3c2a7252cc00f0a6d236d28ce37ed93f16c7f7adae7f70986ce462357ba9bd3a64343bbafd13f4d9be7248c836029a6cb9d34fb14e6fb4c123edd0912a96516f36e81c07b7afc350cf49caca26f42624bf071694808f5d8c09d994ba24bb52ed655f07d8584d5efcc71583f025460b3d3a2a2eb7dab93ff3edc95b0af5e2449b0211bff93e071ffe4424d15eb119d3617a065e39d7e584b504dd2bd51a5cf116fc29132a4eda411604835f25609f019cc7d7bd3bf098b14feda9111678b18987e96ee51c425034bcbe075892fa287accd9fb10ad7c4ba382c832099545d49819711426eb29d266e2f3eefc29de514a405ec63093b8bff6a166caf1602cecf74ce971a42ca95c2c3c28272c3128fe5c4c3c7c69d4f7a4e780b7239d60b78364bc5e6e09ca4e6cb6185d6e696141f00803539a2183310f21f20a5c34e834efd6aec01f243afae61d20130dc6cdd5b405e2ea6c7a58830439f374ff8b15c3ad7a2c400e581eac9efaf1c6b65e6b82222f7f5024c0f612a50932091a76e5510207c76a801c90cd88981e0c654442acb6bbf6aa0cb23bb57726c7ea367bf51d92e21f727d398177cd36e6a0a1b8f64cbdf4c28b743db20e072a19bc19c902f4b0cac9ac52dd74e9734b7d7d346dc1b6d5bb624e011e90e16a1708e9955d37259e68c8ce2bc3187a7ab16b0aab202a2bebb1ddee3c9c01aee5509b3b4739c8de6f7ed049580569e5662856e1127defb9de27a30f85ebc99c3c4c386c4c5c4f793a04e7faa4d2f2cb5018f6c1398d97c0f6c41d86b54ce177397bd9517568c324b90ac86c48c1ce5603782f1bdf39f5f31b9238adcc1b652093b2f535e6116e4ad4f2d6b94034c4ff71108284ee4b4f954a141430536d83241ad6e08df3c2882bc357ac707a3cdacec2b23f396d16c429861fa235eb7d0cf0635a905db5b3265479c99ab391077d5fcf1974999287feb2c590197e4a05d0d6d19c6c20499666eb3100f2001a21177540c705830269ef30537daf79c3d6488f7d405e1cdfbc8c438235a7c46d95d814def7a756b67e0ae90235f64ce3257270b5914bc18e42a050aee10c8e3bd5b378056dab612bae3b0d2bdc660055676a8990dcb24568d9c14e267d7b39d81c4c5c48aaac5c6c53624260521720353bc6d00deb08c410d1fb7d65bdaa828d732cb2bd5b51af42de48b63a969371e68245832bdb5bd55ce20498f9f56ec855fb29f2c9b4b4e26aba30fb1ebaa4b7832222c90e4dcda634fb4430afdbf2720d469505ff823284769b114d388f6cb8baec2ac0c54b3a213dd991317c86ab44e5e7c6f7eee962a7ae8ac4a8acbcd3fbbe33c51658468e18c54714869d06e14c85dd952e5eafb2b158e825f13e824a5ae47495267555ca47ed9e8937e9db22b027a588657d9e31e8c731a2e85f95f78fea962be50bb7d1124f8813496c15c55cf759795bb56b630cfcacb1ee08bd0946ea1813dcc903213df2bd7948dfa17ccbfa5aca8b5907ffcc7229a552453a1c5c6c58ec6c7c61dfc4216f703c8fcafe7012f36a93ca6d385005965d423ddeded366012f997ee34ca30858c8a243892e336ad5e7dd59c43438d5afd3566dd4bf11d5c7f022ffa94a6ce8df2d295d228eefb8663e17f7849d9663064da2280c1a2221127f025699fa109bad2e2dae7782dc8f07089a7c7e0b89fdda8a5c852302dac4daeb854ace8cc800208dbd3a7adddc53a7229e307daf5d0431c44993ae0c32120e16988c100b8f340078d034a6bf3570825990f6fca6b93b3640b3077b919d0d43d26551413296d074423c5eda2d8d34f51fa51b395a0e5a78edb8cb253c042669770f9793835da71aedabba59b0d62f7d3997128b3cfb26fcb2d952ae9de7317078db0fdda6652bc6da5c6c7c692c7c8c7b7e50bbbbd4ec3935c9eae4b5b0df6dd07c2d66ba4588b4a2e80220b422cace071bf8770da2586119a6ff0f6899d10f6a085b1b03305d1b8db969d7f2fb0d618b2d34de8ce73e6b58d5e285fb43981ed553bcf8199a0d2f5de87b9df39399fdc5f4937720e9854991b9a0f971aff8affb937546ed038ccfea3a6d3c454cd429ae0980f3d71008d7172b22df48e6c00b057c69c6137e1235106ac03a5f5d9aee6acc4d85a05b2a3b6460bf5f25fbc709f7641fe6b64fb780bbfb9b21c47d3ccc8a57256aabbfc2973eca8a435b22d0d50b54a039a9ca4de1f51519caf3729e57e9f0053b013ec3944d5430c3422aed24d0977392c22d704a45d2c4d68c27479e1533531f1a9c7c8c796c8c9c85b29419cd5c8795052757cbe5a85673bb95f8058367d510a5b614eebbcba25d8ff9e48e1840a0ba0bfce634361d696b62af8bae2227d61076cedb45869c05eb090845ccb7b3beb21ea4f31ba8ae84a5edda4fcb91ba9505da2b5a2a8deb46ad7f9d7ae15f099bd18af874e3b1db59430a8906de31d93260928d4c74f4bc88f6bd62fdc9bdc1114f46cefa6a57ba86ac34713eb71cf8d6fe502a08e683b9ab8aed29b1b089d3326c2fdd3052f1e54f9d6a1faac3dde354e6813f36d2a2501a586db18a9a6359257d1be7e4a1706cfa2b311503c266b8ed27a0535b69a83b3daf4635bb154a72d6b4fdcc6f05c9b5e927b73852f2e377b8220ca0d1785aeedebb1682ccdadc8c9c89ac9cac9974f931cd575b31b85bd172313f9b096a2d556a0434c588d80aea2e255cc1c7938fc249ffae665806221adb0477f0e2f93005e378d498681e25b494d008beb81e5d4c9691faf77208291bf237245d7ca96f31c06ed1adba9933a1e2ae45fbb395ce7be3d0c4e06663dffc16fb8fdf7b65f4ed238511bc648e82d774ef81825bc4b2fc896293be7cf1cc828309371d4ea63d6ce6207e4d5e7f66d9cc2aa19f7b0ced2185dc2d5e8295ba0afc45739f1c9c6c637182f7afa98f8c878b442ad5a97474c407e5760fea7833ad3e7e0e2d4954ddf989f310a5751dffec83c3a93d1a60fd97fa52f3b3f1bce6e13ece305bab10bd9ada5f2a0db32f04880221c9aaf2c7a0250b1c9cac99ecacbca520dcbb546fb15e57b3e89f6b04546f28d5b8560a663597bd8b346e63886d4bb844d4e853b76e1df91fc4d24592ee21edea8abc50f158921aedb355684851dd8b56195e113a22f07df8fa93dbbe3d005cf1a4dea66ab4f34c7738fce5966cce907a1ae139cd24fd838955fb06ab7a7b8ace44401c3b4611a039f78f497399cd2df36430a388124d3966c5379b589f1f996f9f6c28fafe046959ba61af7bac1cdb150c6c280e23e8edbc1aadab673900d7aee27310f1a128a315f324e724183dd8363c8a0a9f395f77343d0ab5752c6c1b2a4f84363969d630937d741941345cd67c46bf992d1bd1cc25742d5a24b8fc1e1a814ebd9672b407545b2588ae38aba4d2d65b2b5cacbcaa2cbcccbc8bd9ce8c70ddcf480896b5938412897316db7f80a705d2053f1cfdfde75fa4f28bd14dc6ed839bf6b2b40b55c09ac8c28d7dddd61ab5ed7ad5b96785b9594708a423328cd61dd04a40636cc8046e8af7620f93ceac9db418ffbdf1344d88059b2dc9e0f5cb9eb3c506e3d95d558699788cdac0b9bfc7d3995f4960fed076165f3c78546a55d5582db8b66baf98dc84681af0f70c9293080abd0c763e5fdad8c88c3a88acb0e2930cf11af8375cc5d14f777f2c91544c12734c6f56aa3fd2531960fed14a742fab1eb3233cc8f37456882af771d4d56b625e076e3b8b207917a1e01f24a03593ceeeeb87ee151a5d992c3b0a2d2c88c40e23f9793d57b5778ca5913d2ecb9cbcccba6cccdccac74d5e3ff437aeb5dee202a0f2ce2c8ba1b3752c4f9e54ce104334c7cdbca67c57236f9718fdd9d5d3c956c32893adc8ef36f565f09f559b34680948d24f38c547009eca1b64239ec873f9de5571eb66e4e87e1ebb6eb1b4ce5063b3f54ba29fa63aa880f9b59f6685d20c910b11bb0f614cae9a086acfdcb68b318b2a930fbb010a3db9975518ee14d5bcff8a78a6b8fc2693377c65cebb95dbc2ad6e59c932a8d3b4555ea5a5f0df620cda8b1bee381e29bd73b06c1984f40b7982899befddc20c48aa50c5c3579e49b695cf30eb8aadc3162f25ca393b192a49403e99421693a1a1f80f282239f75dbcd39039deedb06e40ecd1615deeec6d14e0baf0857c76c8bbdcccdccaacdcecd09dd1637b08cdac6c6e7ece784d691b5732de8f00c5523e6b53735694667020a93f8e307ebfa7d2a1f6c9bae72cea419f25afb0fc4b4856e5d18b9f19b2fbd4ca20e20963d6c0286458e494e2b24be81e4867df099f6afb6e3e178a0e11cd87b4f17220d876f0b8899207e9df4e489d0b496e77bbade07385f0b80e78087bbbffb93832f8861ce1d4f401849c06a205da13e910430ac4c206615977a04ffddc791071e78c7901081617c2831aae541fea629f68caa1320dcd515b3439a24b55bc23d556f19aa9d59f8749b46ff8c59a3902e7ea5734373ed7fd45406fa572bd0081ecb06d6b5f4b4ccf8b6141d98b70d94f4b9039b4549e50fea57705c8908e6ab1e11a4bd7e9b86c1cdcecdaececfce96d791d7e3831ef924f7e52b432029a225301a783a2288e73e487a3c96b718e5894c08053cd72898a9cf543b30ab2eafc5316dab1a0584c6cd43da5a5253a52db10e4fb17035c0856135a97861c6d7278f158106f96558d5901ebaa5dad5a9d3df48f1da1b3a751d3c12b40c1d49c3a2b226eb1bfb5a44b8c16171306c73bf7f55251edad39d9d5af9a6149feeecd3eaf3b2e2ac0483bc875c5eba7e85c90cf684e15241eb859d6131f34c6817dbffb9e460a1c9938534572392da615f1dee7c4aecc61e235f153708756512cda28f64b69a66d773fbe12e9145aa5449441d9e33bc2dfd67120e2a8e4c52c3d8a859191e15e2cd272cf7e3492d2c60a80fd72fe50fa3f8c5cecfceb2cfd0cf0fa5dfdd441bedfe607ce146b96b55ff69d43d0d03361d11cd2398f379341448415e703679b9dbe4ad7414cbd0bc4309ee12af61f39f8a39572bb158173cf68e8104e1c977bc303a19848223902c32abb9fcf7c9978e2125817ae1e80cb52b4a04db90d6af2e699e01242bf6060c35d7afb025453367dc401f3431bf3fc4a8d0c0995aa52e33a0998e6a51feaffcecb2296f6402f67f07b5bebadf6187eb4eff88623682b2a375aef7139c5b10bfaf7ccd59dc4e861da0d1f086122972837afb27a9f51813f36f21b57ac7a19eb84075992849963f89026dfc5ea54bc30794092536c7d5006cefe7809e4d052efa63dde1b03181495ad74f72b1a2d547c4ba5a8e40edcac981cfd0cfb6d0d1d06110e202442e0a0d68e7f370fbe3d562cdad41c72cf749dd2c50c7330c28c36fcf47c7253bed6042020fa9f1331f26acd33fec87b6339c40c5077b319cc00c7b90f3716be7aac943d72b6e8991bb076edd75c7a5f3b834f119c9dbca664c21ade110bbd6a2f84c31968f16e3957c1924a1cf856aeb5d4794f8454d64e16e01fe5386403b0349a76793edcc12ecd59ef2423b3b63847dc3c9089e204d122f5a6ea63c6f50a4cd41c3b76d15e8955f9d6be40c918efb283e49fb02f1e3be681243a1d58d3c9a9b5ac83daf197cce167b2dafed093d388f4d95f4621ac81b01de8ec47cd09b18979a7638b2845b6f94d7c55267ed53ee68b0b326f50c1851f686061eb917e3cd81d0d1d0bad1d2d1a77899acb721101e1bc33364d6cb84519b3631e793ea301fb9b72c26012ddd7015f7b8ad58f13ce04b08a643ac3b8475e05e6bb4e3edc84ecf589969e28fc5d8b760789e3acc674109db878cf8ff2d824e54b3950a19e53a1e8c811022ed5039e8e34424d65a4293e3356be9836dbfad8c967e574ffa7ba13fe54849762ef32f35dd048329b995c7991855a09ddd6d8d330cc0066d4b0d5d36cb7f631cddd7a6a273bd038db8df74ef237967de534b7ddd6caa04b4b1f75200e937c381de033e88f0da70a1cc1166a51fbc7a359305ae626794ef81fb79edd83c8e21981c9f6deca1a1221b2f55435c47b01f59b2b23e76f929348582fa006b2f0ee89786fd5623db63d1d1d2d1bed2d3d2cd6c3abedc5a724ad40c57ed47a0e895c50e38c19a0aa62f038aa5a11a1a86ae7141055b438860429d883f8d000647111abf4ea3ec220de2a5d6aa58d9773dfa906ec295de82abfd1b1434b9d0105dc20f55a9ca08b795616a2e97ce1a8c3b20520fa3e8841b697caa50b5e73a454099b7b9051a2b48eb5f291df48ba6a8612df1ad159731364e53611cac52e2bb82d6d5b0eb8c248e3e29886b22b414eadb2180081335ea0937b45d3c89b92ada13dc28a585f4cae76dcc723c0f726faf949f95fa706ed55aa840ca2dee7d5d5c72238da6390143a63e1c9d66bef9a3be358aac19f20f068586cd65bd035a04759e2772b39b65512c6f1c5cc37ba12db16177612b7ef6d5d2d3d2c2d3d4d36796743becb7857e553fd1b2a68fc835fea08aac6bf61680b414fb269cf485b51a91b5f87a1e1db0b07eb773b6acc2723774b5279e221d810fa5439aa70c15a68922257896e79a9d42819d1f681bf23b119fda918f162866a2cb2a77b6742d8857512dd1bc3996fbb625450e3b187eb68b5d8fc1ffd136ee06850e18e8de2e9102d85a87322ed5549fd25ec314aff40ac72b25e1cee028d6e550fc8255fe79b7a481e205d64fe7366a32dd12d0523419efd82f44de1ca4a0a3dee5c5a242bc61355039a20163d474dbea9ba20d76281bb39f02cad3c1c9615f96a9944b1b7364d660aca2094acaf6b0dbe98aa47007ccf76306d44e840b390e3a2259cec85d73f4839661d9d3d4d3c6d4d5d41d6c0fc46830895324fc0f59ff3bcd2348e3987ccd36155a063a727af8d04b37b273a0ea91219ae3e99340e1d9a68a0f06ee75811659e7d912c2c273735621d4ac32018cec50d62acdb49f6da6ce8302f78ce7829db9220e97bb0d3765cfe7bb461609af77de901a218630e3ede55b96947bab050aa2cdd5a8cf05b331b2030a64245a7c26a0b773d27c4cd31f3f975b62c3f6ba18e6fd00b13d480965718aeeb553b52e27669f321e6bd4fd614d55dfb28317ff9b9e18c494fd3c1b57a39e5d040c6343c8b4095144ac5333498c65b08bc0c023ace001a3f01d875ef2b4f9849dee99cb09578a5a42d8d071de9987987e9aa50e5287046d8f4ddc0ebb17f85f298de5aaddd4d5d4cad5d6d58818de4553835c59b0e147fcb544d5108242631483001d10d12f7430dbf357b9cbccce527933506196a54f4a2cf0b861497072a9651017346539b5ae503b6055848152e8bd37b6499d619a19b7d94de8e2293a4788267ef3c3f31d2a912b36cb177acc4b0d21a87b5d0a9a54741c3fa1976106f158e5e5b122effa25712329ebbb31419a2e102f18df5d20d572af108afbf3b55da759b99a966a0be7b81fc62eae98771d9c929f98f25331cceaca435faadd426ffc81347f3170b4d3f642963ea03586ac424da379cdabe808ffba43f2817fd47ca8c4ed89403e3c9d6c228029ea7336d04801634e4608a15d1e96b23f293c8c322e082f9e4374fa88d7511d4379d62f6de1d5d6d5ced6d7d6f35abc671d7c0de31190a0c2ad12244350f50816d1f1572f9a0b555ee4b6252c34c7ac54419931bbc564bcdae34321f7d04e6aa088bc5d2ca1a3f0326152052b43b1b6e1aad22e903169d529ba4c28253bd06dcd97c6cb9fd09916396995756a079e18e74931f98af0332867a4232cda6d987a7510948851dad005c88a9f00d2a2cef3172ee90e94f7be8c6b3f5e58f26319cead2f0555ad53091661898747475183de54a9eb910562cd6a0c08d2daf43397c533b22e2816e29ef1fc17bcd0a06510c8cfb06e3371c661c942bf369765c09010dddcb78d6f62327c99d3b0716d31d3a789ee2ebc28abfb1b09d9dca310d5cdab52c2a943bec8ed4252320bd726b0b717c756e5a9d2d7d8d72ca3f52ba048db2283f01251df3d99144dfeb6394af2694d8a3cf21ecc997d8a39dd588163b9604fb8f9f2c7c865e210b5f54f01da662a91d5a2dbaa2818728446a45d8ff3a0cfd1240acf194a4ea5c57e3c5e7ec6aeecbff8bd00b1f75ffcd843900c2ca098c28a252b53086ec533459d86c132fbb24f4123b2adabf6c6d388938e91c8aa0cf3a627a2426db0765a35d3a25425e375ba7c515acfcc474fe17f88afd96c6b9b4a2ce1416b5d4cb8e6bd00e443071d9482e7eec537b81a662cc55bf86bfd2c346c330bcfcc6c2d5183742081575f548fb1edf3e0573a10435fc513f70dd584e0035227f68bbc1773751b404b487dbe3dbba0b1c2a517e860d593fe622a2222e9d7d8d7d6d8d9d87122515ea8fff0b4787569c37ff3000c4d82acf50cfc78b68b5b49854b62431685dced039efe21f4e4a483519860710987e121af81af3eeef6134968544a227f869776d75af110cc3bb817bae31caf7cd09e1e521b09aa1b6d1f047f911e96fe0af50e63cff8e68f9a9987386855e0f1a468c99f07469f365eb88f3f9549444a7a171d6de4f0580ef6be536a1a027d620fc6a9cdd0c7273245be8117e6d13a5aaf4910fb4ed3d3afce6203e752e0c12a0a21e2560ece30ec33481a98b05af8dab991e359fb6d248d0d00b651f1ec06dbb310880ff351bea7421bae69aaeded356e94caf30ad404d0a92c1cd5a2cd52ee77293fd1982860959e3b3e553a48791cb52bcfadedd8d9d8dad9dad975f4b79788c8e687428cd7a8e6d3063c3c6cd898f0c71765a2f1702f6b1660d06dbe10393b078a9ce2b30e5e6febed03238a0bfc8d2ff855bd35baf578cb973382c87dbb332fb949ad36987f8f1105a0a1333be55e90d475bce3702014748ae3716447adf0551e27d17a1dfae95b49abb80a8c94bf8093366f40aa1eeecba571580551be6f5bb76467d65841bfc1ceb2828fa7bf2df86f8fde6ff1f297849886acbf2a0ca38b03db587ffdcbdab35cb33d07593ac14521c67ec4db9eba6d68d8076b0cb9b04aefafd01b6ec66ffdbbba977345b9381f14b540f16d3ea54e99e61b4d3e698bacb719a5fb4c2193f883e385dce40fbeff6357c205e262be472ca641bd5fcaf1d9dad9dedadbda1f5bedb07a8507ac754296b8e929055a35a1ea64e3fcc19a80bc3fd2f13176d195cab86c153c46551ad326613fb387e922bf1d27d1db1771f13ee5cf3045289783909c37a4a6d29a158bf1529289cf92b1c9fa77e2f226f7b14eaa9641bea1d1ae5b9fbb2cc6a7410f7403dac34bc8a996afb375f7f2de60be8fac0a892e732345460fb1eb4ad19a4b4f9975a6e52546bb3fdea6cfe1c921fb628952e7ed6adea2ee57555f98ea9e51909a7dc2d5ed51efdfb95fb064a0f3afd9d3a228527ce1359f25d0302fe3bb013ef45e34969731b3f6c1cb4f4d5a2819c531ac9ad69b998296b5d0419ee12ebb12f9bbce029d03f3f4968d9c5777bf8018108c4138b18287b831dff5dadbdae2dbdcdb9e2907af90207b266194233ddd72bc9be68e51d4e7ba87db5d679d42c2ff1cc51c1f10808dd6e65745d6fb31e74f069344ef657598af76918e8ee0ec5e5e6879884ec675c79911f56aac9c189fc789f54836eb6a5800ebf8b9c8b3c63ddcc30f17aee597f43e692693d0ffed842edb8b9122084752f652e5db400eb34aaed9484fb55c28e8d0f16750ce9601a28a4288f624e49d6c0df5a0701b8e952014e6d6b63578e05f15afec9f112d0085f570a3699efe422dbe35e71c744caa53ca2137775f472aec8a8794dbc9957003dcd36881808dccd6c3cdbfd2723c7e638bdd3db0412ddb1e8733006c897222d5d55318e355801440a1552833fc70f1d8c91d21c183447ff981dbdcdbe6dcdddc2807af1e53b9d3c64881a6b4b9858dd7c83c8f558dfc504c56cb898a1c403d401c8043c0fa7d88d5a305130ba0bf81beb5b453705ee477fde95a323f4dd4e8bdb79a28802b09453d18d48f5d92bdb9d7e8039ffab97c1cb0c5dc75a03f31ee7c80266a73c98101e219f9ae72733d8633a05ed509796f9150a1e61190ac1393f1ceda286649c5005abbf39eaa18ad9cb684a1e08a5a7b1524079b63193aa9d612b228401421bf75e306d2b73129b289323dd505c06c90893a6472ebe3658759dcd1a3c5d01c63b5456d7f834294502886ab1f2d4bf2f719ee8dacccc51410f1ec415630cc14c67c59221b887497347b411b414ff2292f051edae371c5612ab820cbc4dccbfddcdddceadddedd1d646ba42a53dc8051cc425d07e939241cffcdb6fe6f4fa85821590140cf4959a1bf5dead701423798b1904e887d303dd0e26adeb7d77d5c7b988e41da11649dae60f953150f8d093ae1d1c8caa751492e768c604c821e824dd8bb94e968f76cf4abb62cfa8b2ec1128de4fdce72c5579959e24f372879624569efc355687233403d6291aa9e2a50521c6a66fa0aa5032c07e2033d56891f4ac0772a689d88dea41e64a007e61b12e3e9bed9b599ad85f242391072df023b69c229f8e584f2ffac4b222c40641c0175574ab499bca4718ed522dee12b3882e848b59a9b63683a1d55bbf4542c3022b1aada69a41fc2208b09a9c9437d86523e5138e3ca48feb34da917538182dddeddeededfde72c14f1ccd736bad7aa71456840bb8034457004e0880f08030800b7d1380040132e80002101283444044641100100020509c8020a0050e081e0846853e30806be189f30be528dde403208a6bd0f41afd07857053455b5a90fce8b420bdf38bbdce927e412d9ee08307f5086e0c45f09a85136d2168af3369e18ef30b9a90c9b306e31ff30323c06b21155c23392bda9a41d673d2e21ce71734211b292988a0298e083894017d70ada2ad1b52dcf8b4a0d4f94578c8603f307a85380e2a14ba699007d70238f350e086d3160be72a2a5a50e4fc62f5704c5ffd41f3c023707b502376da52035a91a616f13bbf18687deb6a705f85c30f38720acaa3492a4c6e79b3006d2ac0d01de9a08f0b6410fdffd369fc04cfc7f254489c2fda5a403f698639f1d3f905cb614caa82feba1b16a0d11350063755b42583858f4e0bd23abfd83796ec00d03b7b2ce4415b38aeec68eb84210899167139bf603a38d37a70af7e2c3e83385c79c6d396085376b8166e3bbf1009f3e946fa1549f02171db7a04a3616d1b4f5b02b45be4b420c3f9456660469e540d9a045813258e9b9ef8f349069869a52d3c0057d1d38232e717e5219dc903602cb891b05e555b5a90f4e6b420bdf38bbdce237b63900adcfb3070555b2cb878635a90e1fc226364869f0ca1391039b88a5036b0afad01a7c0392dee707e81754e46719ae4a736983ad3733efc04a162c01d5cf3b4274e675b1f5de0615af8e9fc02e9e4b6578068d5b57807b148cbcab67047e73ce6d4e21ee71706f767d4d4eb150f82dfce1fb424b8067055b6b5036cc7410b729d5f6804664452d0a3cddc068dc8803e149a79b61cc31f4d458b089d5f0c1ddc760578155c45d7d83d836c4bc1961fa6851bce2f64c46cb8d40f21282c52d0a82bb806efb442959d6d0d70058dd3820ee717ac8731a90aae835b8dd299996df930822dd3226ee7174416b357035e06570b2509a062c4cfa235cf30fc194bf40e3fe837f0d97d2c48833b38666b66c7c2565bb047c907550ee35a50eefc62c2fd5e5e35e8239bb9081a9505fda1df17bf73d71604949c75b408adf38b3bc388f880107ae013ed10a13ce0e002eed6c97168015f8ff10a296e4c7ef2cb15e717f29e1522315f2c604e061228140de883eea9bdb67454f1c9b438d3f98548c867970062c18522aeaed03ebeb64484f2c9b438cbf9854cc8dde506c3077d18b4053711ecf784065d40388d6f16903cc3509625e0069b057ba0b28745f238ac0472f1d18e57998c59c0effcc2c07e22dca80aaa39e7ca1000c7430c9473edad18372779b380042ac0d019297d0bfe771b23501134331bf4051cc8826e28347b6c39873d4d854235440a2fba2c16ea5841fe4c797eca699fe0f35136a519bfd3d601971899167434bfa0abe6e7d814f494686e43b49e82c590a69ab674883877180ada67187e9611190ae6c105e3ea3084b67448f18d7141665780d1c9715b6e686086e0aaa174008b3a04f883ea949fadc949d5e218ce2f78ba9e3406e80a6e13631af057b6cc00081bd3826ee717a42e67fd5483bb424c95fa04cf47799466f65bd872e00513d3220ee7174c2e665c0beeabc0ef81399f8262e8cc236c512cbac534b488c7f9c580f5ecd58097c1d542d9c33cb63ebac0c3b4f0d3f905d2395c0baf651b02fa6778aef8039c0c47b0037265403e68f5353fc7d842416cc7a10579ce2f3483e0bec22829ffbecb4d4f43bdee0af7600c4ba9632b0311a6712d28777e112e65d00f1a0b230801ee318054c6561ce3143e5a44eafca21848201eaa41194082d122068d64c03e68e3351ba7d88a4258c3a14578ce2f54026ed4cb6391030529423be2e01c94a1c902fbb52e815d41550b8a9d5f8cba82db166baa0693613a27f3092e1f65a5e2bf6fb1e5c0095aa6451cce2f985cccd8d76c0b0f3c0777232056c456c7283d8c167e3abfc03299c9aa205ab7ad041a3d016530add4ccfec3d68713389916773abf403ab9ed15205af5aff80e62b1ba1a0b5b2242f9645a9ce5fc4226e4469b5c8b39f091c662a046e760190c51c77f9f60cb0523c097595df0e1fca24660b816a32f950892200270d5a0ec66035b034e81735adce1fc02eb1c168b266755e19e019a43717f8e4620200fbfbb3f2109772db9006313d32232e717c541060a0feeb216de6ab41ee4e1b3ad99057c2d1744cce45a5ceefc42381e79a1de58ec0e33f517413464e40aeea37b576649f65a075d306f88be38bb1089f9113ca0939d8d40c03cfc62ca3ffc2ec64d330bd463c8bc9f5f9084fc76048880128e80379e83ee555ab502f9178721908828e80bb4979530a004165d9a5ad38ace4e2a3c422ae5aef501c4012eb5dc0e3f7130bf4ffa6bf93fca6de7818008f6dd293e832495e73bbba0b7dc76335250c74165eb8ae48bab1e54985d773c5ae98bd7fa5bba6b156b35021427020ce20338d646fc3ddd8a6fa14bea2598c56922e598fe377c8dc1d19b02165d3c1af57db096e26cb89dbf6f2423d144ef7a11ae1510b60ba1e560d78f3081063a8a31b411235aadcd71135fb4b6b365b002fc4c577f8e3423e834bf4369fa106e2822e5cfc294588eea1c4942ee1a35cd548df80f93cee025077759e926f581eb0d0fca1d4d4d050c7ea030490c722f6f032c6614fd3c9fb15de45f499fbe33ea80ae5d47a8cac20d32620e391787ad630969cd7d9e12ef69a5f3c0cdeffa3718bc7967ab8788b2966ac394f1258b52633b1b2e62dca3f48a6c33e8134345c82771eb0d17b3f0f239a5729c54c03ced960f0971fe0bed58aa0d68718582dedfdef2dfe0df3cd7deb06917e4e5bffac761438e477217390fddd0e734d4bee7b58aa5462a53a3359ece9ed8129827aaf716bc68208ffdd00a9202232523a2908577b35a7a24a3bba93c1905adc97f709e14d80851a72dcd9b77972a01d714bbf52c83c1cc605953941e3f4646040025a689acf867a6a3a86eaf335e418e1d590e58ebe27ca23c22fa322e11159bc02cf92251f3ee0027caadd785cc240e19d517fd78187adb92c62d1b833f53f4223c4bb574b6956f4fed433780a76c1a4ce8f81ce7ed64ec8aaead714e1d571ed30ca485260b12abafbe43b2b5a1995df31eebc1b7c0bf0074469e42d688a32bbefd434855d95b3eae4a200a02bcd18c89310f4f67df55268104fc4789dfe0dff6e0e1e06b45aa0a37eb1d73f6ac80e02b267b36bca8468ce6d7672946547d92ea305a3f37c749d3cf9bdcff0996fe4e02d390353c3e0b08040f6f650604dd6e00ee61ebcca1c6abdbbeecdc2be1ba00d258702c191e8eb699f59bdc3db6f263cfd7004ea9bb28476be18f60f3209b69951f459494b47ba1636b2be717f1c6be1c71e12c0fe3e2cacabc80d246a0ac9eebb4f69f9348777c55d659c3eb6df3be7c66c04d88ac4f318ae4f15c5770378574ec051008918684e4f46ef72f4c09414a39b65ae48bbf2472355703c273b259e79c49273487de520c0b349587e33bf13efb11266417fba6f0c22c783215594ce77e98d1a02809bea8fd952d41ab2748ae233a7f9ed9b5d8bd8d82e0e1e0fae1e2e1ff4f2dcd541cb36dac0c6911afe0c5d0ad47352cb7d64ffc39d16ac6ea7e146534189f12fd750088055687c3e2588094ec3a2d0a56fb4793d35d74baf485b2d79490518c183628001857769d5d8b522f4d23a8279d4ecb2e4fc16864ab10a2777a2f245122d6696a137555528b1e0f7cbc8b67cc5822070a9586a5b301115a92e2adb5e0fbf0edc0b8441e4c621ca6f1da13393dfde45193ae92e774725d3d1d2e96d3b360bfd34eb776ac3297394c59e3ce9a14c6b503e6add649bf69aa08e5a00306d1d46119151f381b853f8c070055b3a4096eb8005670fc1fedaff6eba49a3a859b474f70e6cc0fdd52ffdc3bbf839db39cfbaac07471bfe59e5ff02ea1f069c9cc1c91e1e2e1fee2e3e22da1f253c5ba84469cd695bfb766d5838f8e06af989a826da11d82c2c7f6b81836e2a38e7c46879f3fbe0c088b4f4f5f14a8f715b60dff71522e0bb2b98fc70a97b3a582e577307f364a7a28b3b0015b2d9f04978c930f889998b1d41d1e8078b869f2a31188e0c95f1befb016dbd7491da52c7242c4ba590a0a1731c9f2a2002aafdacb141d43ab1e2f2c26e845892105ee382ffda372beff72b74b559172d265aa0a1fbf9bd99a8af55f946d7cfdee0eb0648ec8e8d4cd208610ec76f9561d14f25fd9bf38dcd00fcb3af9820a59ef7599208ba2bf321bb279608a46f9998bb12ba7486b064bd91fda7e0c45d589c44826c9360bd8a453b027de1c6f5a4e7c8e2e12f5ae95e2e3e28273e3e4e35ac846cb24e890a198f9da7b835f5ad728ed503b4586fb21eb26a8a5b36a1dbd37e65932225fd0ec4e7106f758ea77d53d3840c918993839e148c5eedefbe1c4c889d84b84266189131eac02759c6f703eee65ccaacf9453d91e32d550891b50e8fd6e1920e3c3deec32c61036c33fa0a68e35e5b712e635c351b4f98a8ef39fafccbaaaaddf1710c1c864ec4297a3b4648a5b309f019388f0a4bb4e2d6ed556c4a391062f1f19b881b81b20ca3293a13c596ba61170e20f02a0d20fe96bec8f0a3d702929d29c6ad6faa8b7cbe4e0d4898edc03ecd24ec8a37203101fd674aa4d92bf1be1a88c709c56e52f46ca57610b2e6e6b4cda9f21f622dae33e11aca370913e28f49982e3e4e386e4e5e4e0d02d66b7ef3d479690b31d5a469fbabcf61bb3567687d9ab9f4c8cb6ac4b1b0d92aa831c0439f7daa6f3d264fa99b20f3dcb938300032bce69588b86cc5a8daa489edcdcc24ff379e8775622cbc9737f7bc9e698c7e66e54e3a9901eda64818060b7c408ff04778fe3dc245772f813a6d6afc1bff6f3f9fad80f46d21c75abef1df0d55201c799337c4c897c289aaea1c69426f8dede7f250fabe5c799234395ece613469447b5ca9eda403e550d3d77517802a4a180ecb8fc090bf2c48b7ecd29e0d665b2e8e3d1329a030747e7498b330794deb1c6bb6fbf1574f718fbfd1be28ca81db36be4a39a5cb2f1e381bbb57b7a9a1c4fd1670ea1400b024f8bdff98bba6a9de4e5e48ae5e6e5fe54d0b59780c5271f55d7507813996f49b77904706692a9bf24efb327b691b4797c45d15d68d4a4bf220d09b44a09996213fbbe06b2a110bf61b106faac71ba835ffc5b5966b7858ca1ecbe757534aff67d995476ea8113d9d69ee5a34a655efb5b54727590606648b3abb3c78b6d2eaa5eee3f1c3ea54a26fdc6dfc8dd94666764188909f719fc0da42129b78c7439e6de57a084c8e3849e2f478d51c3fe39b6fda0d9179e509e39e0c4ec87b03158679a752bd63d1d40eb79e063eddf4a28b311232b3db4b0c29439aaca260186aa8f42a0c5155292aea9b935083e9e47ea446e8d3c1d1e843c4a19bd13789c41210f255df9e8c8f58eeeb4bd513a67e818c6c5f454a182e5e6e58eaae6e7e6fc8cb5781510fa19209f3c35bc227cc95d96155285c2f4fc4ff77271c5c5d092372f2e1bd68142a95d7a773ad134bde3a864e54464327984fd52019c6234d1dea6901fa79e4679c36f1287785896ac6f86f396aa0f0bff4b8a4d48d1fce1562b64f4c9670c5e5fb88e299fc63799f000f2a43c289eeb01a5db3ce8631852fbd655af30c7639b6842d445235afb9fb3380b78139ecf4992a63d178ed9a96f01ccb1acbbcfc84257a2b70d5aee05dfb531cef5131ced8bb7d712dff5f3c5f487baf3b9c349baac8eb4294eca2b3b027ee90d9257fb93f2d574766c6e6a2cb6dd63e5797c5ee57be8f411923af2f05d49019c399e68b010112481232463ff9c941b08dbd9b9f4a582e6e7e692e7e8e73948214d2f462a2ecf03d80a74e2747f8bfcd1cb6c25331ededa14dbf771124033ad5f85a77776c21cfdba382b08470bf08032435928d02c95bca2511d6c0ed495a043e975966a11fad19c92dc454278e985660a9e0ab0c4093a6d875131e3fb6307902ef6c0d880d9ef85ddee575fe4a2a25f4052ef4078233ae8df4b74a099c3f0b9ccab5548ba3500dec5e5a7732216b1f51a721595533f7ba7f40b0e77f4e593c102ea2ec97f5e443680e62b1dd048476cb44ba0628a89d0bcc066976c8db3993502a475b627a500616ec60ed6a80fb7d0891a0f7d4a780ee86379e19974ecd4868518e031c5cc98490e24401922251afcd47955b770b93fe6e74ec893b5b3aa96ddd3a9e7e8e796e8e9e8718bb52de77713c1ed5587609a0af721a5892b40f36b9cd0dfff3fd08f99fae2bc5294adb3548465e3a05f1285119acc4b2199a480776c0b913e338b9f0aed4b9503f55fb62553f4fd7a98fac30f9d1ecc91ea3704d500be255a035f89a15994298ea82d132c38b55af625429fbf7f56b1abe1c7c813fdd9eeeff70e29c07898b6b527bb716cb2c9c2173d4c2803650b31d9137b1225623fd3878638cb70803b8e0c94f4da0b8ea24495e24f950a5d413de59a5c172423093f64ea2d3c89bbc754c61345ab349326a4ce0ff933a6b3ff8d796fa9436ae87ecab963fa7f6fd7ba00d4b6f8128f676dd7a811801ec0db79d31011ac3912160ab829dbfd605ef9f1a12b334fad82e8e9e89ae9eae905427a7fe902b0477de7825209fa045cf44c10e82ca4d0c0a272ec8b19363c4b56f6c4a81307cedb1e983bca16ed4488afdb07496ed0bea2ad55afe56e6f3ce08317223b01402be14d4f4f612f0a6554238493b3e9a94a813bbf476e26bde6721a0390ce0f11a2c379754f9334fbbab2ad6509891bafafb57b031073fc157af859dfc0083dfa846747ff2d5ad9a00de4837772db4941c032187c76612ef349ee977ba806e91444d7c6f8619fa3ef5ca2c4342b72a769f3ee7f4dd67bdf101a37d5a13ace8f2c6dd54b109b0702c28fc09036570bde88ef7388775263adf92c410ea10873271b9839d1f99b646eba83cb2fc70dff84c1689b5570ad424ff1934a680502f1b182e9eae99eeaebea6b8c5f8b588ebf960dcb4fab49828700cd73e1fc601a23c6df8db1569375cf7b415cb9a6cdcdb2438be4e658736cd207835e120f650304db353327cc22cf8f82a02805b003fd3491937a411e47da036517eed0e7a8a9ffcbbf9e00d5067c334ba7b19b10d4e13f92f075286d5b9110cdb819dfb6085b6e517fbbf3e880ccfdf98c14572eaff30778f108f9834575f1c2d79d7ee44b9be7b1ade31b68fbe3770a901b92d826273077d4c6e1de1fd45611094e3dacc3b0def29e5bd4f28d8b557d86a3d37b5a72c8e678b292295abd6c088bbcf693f67d2574eea84b7c0fdb9c670421b5f629fe4e9291b86e033c2e4af7db128ec4c06e81415d7683bab3a6378ac7e39bfdb5eaebeaa2ebeceb1a1c71ce3c24e1dd133dea8dd5f2e250b127e732b60022dacef4c71a80c07e78e63e2179afcdb98d8f60ffe212001ffbdd7d6f7638726d24846cbeee25b6bd138add320f9cd1afbf513289e726dedd476b09a87d8fb6d2ad98e174f53d5461dfdcd26a1e5db4d13e4a7a39741bd150bba2e00a408d8be14876d79457dbd7d699bfe2c2e0142888eecdbbf08352ce025fd9a9f9f97aab09ad2a73879c616b7172a83ef0dba7c04e6b9263312470af6f2bbc2208948035cf11dc94a255ed638b97e513251c9f71253a4b10fb0c1b3e0ec1adc482385923ee008c0f8092df90cd8da1d1197f510d67f7e3f619d0b7d7d40ddb8deb815ad23b25c7c2dfda27d124b60a4ff11fb9ebeceba6ecedec2a6ed4a2bba76eb82fd1f07244986d5f141d18a6ec40e4d840f664407f14540300f30540c80016f1da476d023593e7b2d5d5a7213a52c25b4f71475a99abb61cae294e007873d2bba35b39c744ef192054ccc74b90d9702aee44a2062b062bc7c88453019be43b5dd175201c213eb4edb96396c486aa71ac2224dfb3c5a94400f674360c91e145c23424b6563a6842ea8c247dd6f5ec65fa0154b3ec1c3e33a0ad7d299b8a1948cf5ffa31a265d6894910bfb8967ed590cba620dad2a244a4d510f05ad4663ae64e78954cf199032b2890a9f0fa09d739f6d6d48d90355d385d5527501bda4e1e641529314df4720e86502fde5ae885428144c5f34cb5b002ab0f2b423abdecedecaaedeeeda607053bd0a8cfbfb295b241ea77cb60f0d87a962edd98979ab412cb1e35b8b487bc2e19e95175fcb5e01d5966102c14557963cc130dd19e7dab1c7f35eaaa6b8fb9aeb66f97673b884fe561257cc00dada3d6033aa5deac27d599c4f04b693b9942b513b8c94a02d53161e023179aaa8b8e26f9b0e30c67361930fa1f38796d9ccc21d2c703b0a8e0f09c0cc6461ea0b2cdd4b5beeb310d2cc96b42855d070493f37511bcaa9aab8ca36a0b658059f9b38b8ba6e3e7bbd35175e259a254c4bb571b38b8483f80e4de4b93ac60debc3db73c2e60d6084ae459b283f2e060f80d760148c3e8bf6ddd1494ddc4f4056c6d304be46049eb1b434d176b98e88234b71d33bee4c1edeeedaeeeefee91665a84e58ed4c3799eebbf73100366ecadb7ab4494c3d99f4298f52c7598fbcbec5814955f7f832e328b02bc3882f1b21c877f8e9dc7875407f95101d3fc5596ba358226c3e09e596becc060de59c7a24a1a3497a1628f718dc8a2d38281da63bf703693c92c35fb1168f42dfc1a58aab1395ba8364d64f48b0c2bab872e6113bac85d47cfa158aab8635dd3d355920e137e0034165825dcebbc533ef908e2b53e8e279ee03d2bc3a17b075fbf377f02a67b584cecd3f7d346aed62857bf439db8bfb7af6c8c393d76276c1fae416e8e438a9dbdcb36a50e9ec26058aea5af8a997dd6d839733ce4085aea0e2cb1e1a4484803c9a6b1714fd0a2654380c654750d8dc582eeefeeb2eff0ef316962c6984bff773b0d6d8733a706c75cc81f2ed0391fa9befb6621bc760e56708d6293c61f63fccad458955a5317c2e569e5db20001fa75f070b9ca9ca07ee9938e247457eae6e99286eccc74a9522155a4757bc74e991e3ebcb945772f2c480a2105b8930f78834ecb852a5ca48c0865170ac51bea566dc40d1f1771d2ce3ceee0ba1d8ccd5da871af09d44fad717a3d53b091c225963fe2c3531f1973a89a93a9185b17afd3cb844173982d20c7f4b1f43b31b3541420d7d1349fdaf1b11ee4c05c07cb5f83dca92d56309299495adad2ab098d55787d2258746cadd8aafa279766ca302b8c5e9f5cea63733f7296d7dcebc914494fdc68ece89468fd58840b9097dc9eff0efb6f0f1f051b47f12c6625f968255cab87398e9d3f22a389d70ecd2860376d5bc279e9768b553bfca0bba75e5f9ef67acf020d1e028b5d660298f2b0bcbd4457f874ea39c903a843930945a495699bb125c5025713786e77c59571769223b7a9ad92bb75a97d80b7e8147b0fd2900cc92515fe19fb524adeaa3f256b2e32ff686c5866512b35af33ac2c00df28862022a518b65f4f325a8cfbd7b53d9fd7b03f688f38384a77fed49d9dbb8efcf80a2a305e1b306552c4ca10f21ce72c058002dbf0c01b982fbc4c56d59244bc8b564df08bf746785ce3c06b0d8c8113b8d0c4b3c96a94002ca22cdd822a859f38663f287b253454178c700e2659b80bde5660410c27d88c2f0fa66cdf0f1f0baf1f2f14e9d5be3b93f50042f634994028858256efca70fe5283bb6c9fef31730f920ae829ecdd121dcb57cd1aee8eac453beefad7521a777ecba3b6f96b7a6712074b7a2716ee291e42d2a1296878fe13886affa5a45acd65e4b3c530819f99f78ccb5be7622f1faf2d6ac21e5469e7839ab648b3b67ae0eeca6ccd25c2052cb12f2a593a133561dc2d77b848458bc9b6c4b8a77197d51e3564ea2540e545782e26b32ad9f4528ccdc10b8fed0cb442b3cd1e8ffcacf3573ee5f7a8ac282d4a668b939f522bcb877655293fd1f877a78d54320a93c456e52344181a74de8b73b38b3f7cf1818054a7ac21be6378bc46b6c0c045f4b5c033327ffa2c605a5546c2d74e558b7ad59d1f1f2f1bef2f3f2b1f70f207479cad371316c9d8c82ab643c7424bd2976c4fab7d57f360b446b0c0ea6780535ff553d4045da51519c7e02c9e0561b54bb6c0c0e6485111c22863c96d36f424a50688a831c559f16ae4ff3cc4850e54bb941e2a8b1b8f0109166652166a74358fffe4e21b113dfba40b40195a3bb64f0a129df78b3f8f13917c772ac1e96d0a2b07f4dd21fd3f4c52b357dfb23e614ce2b447a9e899f63f7217562b2cfdf472b3035f203c2b82d166a7af13bf16559eb56c0e49a7caf9d1d3a1a8ff19886674df9fcd26b87a69cf5456f2f82984b66f5e6968d3cf27460b2cba06568229cb4f3e62e29a85a1e1bee0d8ecafd5e0fa4510b3ab6646ffc8956b411339e078372d5f2f3f2c2f3f4f35d7f4f96c1bbbfb113cfc6a6b51bcefb0bdab315d0ec474e3344b647228f43290d2110d3c44d3bf89214112b0d0b2c2b1b21ddbb13cb571a77e1b761672bf90c8716e3a3f852b449aa6eb719b296e7808a344646589304c265fe69600e462157626f2da50116ce1f52d907be2980a63ca33bd326b27766bf8d70fc1235783dec1377a5155ba32d4e4f93edb785534655a23d716c30465456249b435dfec6cdc3a13b6d0938482db769478430570702af96f6cb38d98d1be16395a8b71394ff85009e692f7a6da454638dc66a04301beeb07ec120557aadb03d9e74d78a16f4895b10c8dc5fc72c68721273d7b7974ae6aae25ecdd27e4762494a4ff2ccd5daa8e9f5360ed982f3f4f3c6f4f5f48dc933445f4f3599522b2d12e869f7354e75237d2dfae78017499bc9ea4dc23fd592f598a05e0ea5fc917a1d20f50a8aa5e282815df90ff6000c57982f9392eea3be41824f61a4607e803ed6c204d3b689b5bff525ab4ea817a0f72eec9618549a1e1bb3177583af1d1534433eb8619caa7b1866cbe95e2673f2414a0d85795103af16bcb6f9817b565d6c6ca7322ea5d926c6c5f62eabfd76faa7d16858c17786a4b48d094749aa99a43bae1cf1a3c8e71a59878b546e02c776b03e98fcdfb40c32fb780877fa3722e90df2e2af041e8459ebe19f7d2b50c03403106fae1aab2f0b6f506e7962598bb2804f429538079bfd75309da1044202b11a06ad69fa573fc5a10eddf4f5f4caf5f6f57efee72a3d754192ef884a978116a36db722ad2839ef0b1ecb7c8c819cd2a523480ce3cac3dda15e9309036ae5016df937dedd52d8a1ea419ae0e0dfb28ff9dda4712fc0fce99837edf6d001add6cfd1d765adaad615ad06a36c3a94a67c277fb4eba4f2907dfa348c8e108d68bc1b758e527e0ae07a9df12a5a19fdd40f3171ff19ef943caee9de0747374ad38af085fc020bae3de28d6ff4b408c28a616c82aed892d3454f2f67b07665133dad97bb5836dc7028a029cbc445099e6de2ee93bf4a42a7e38ce4eb8838780d1908e46099a3f73d2e9f53a37b794ef22cc7d0ba9cd73b42489c08ecf04076b564bb439c7ea6e6cb5af453cd039a1f191e15fa0bae7bdf02e1f5f6f5cef6f7f6577566336ed2506d7df7cc3c3e4d8167245683146c43609f079fe2bd22ccdca019f46c3c0b47c5a10daf087d06a6ec25416985a3bed2515f13b3e541c47d34c880e8ecbc1197a4c9149d143bfcb941eec1a1970a9292621e0cc4ab085a78cd466a9233ff08d521c2111fdac18a83f85290529061f4f4fa84f11c0f5f35d92738b20ccf20190b26f811e666d892b192858b6f098ce8dc0d25bdf23bfd652f5372a257deefaedba6a7bcad0c7b3d2755dc23ff89502fae2617d1d27de20d21249bb301d0a8c1e278cd4b1b496e9962fb0b984558f02f7a7d3743ce2619677c023097aafa319f8c334ba6e01000533459debb1bb55b8591f3fb4928559196477c265e296e56e5f6f7f6d2f7f8f7057d684f5a5b9ad7574a5dc378789d8575236ad33177b821b9cd13adacc2847a9dc15588b96243d191b60dd204614c9b69539feb896f9be898ae5e0fffa897f58d257a6a2af6930b5d3a456dc0c0c051cef8ea61bb61a85d8fff7b8878efa6da8c2b58d512842afe3f393fd2ab77aa4eb6529a89a5333738a71d5bc98f79eb0496b57756bcadf409674434aae486fa2a754bf898113684489da36988461aea32a451a4036232622166e21c935dad39dcfbc1a43c900ba7842e1d55650615faf0175893d531dd24904a22f78fa3b0a48a82155930a8017f55fe9dc557c15ded2489080980510bd56efff45660907daa25616abe6b396583ebbe114c4cb23e6ec75a983a7ce9f7f8f7d6f8f9f8fa26c835647aafbe915e8a53d2841d80d1321a8a01f2fb454727470f50dc578683d87c3df0f596c8bb6bbf327b3b052521c45fce72f462a73b38ec43362a7ff0a2f9c58d20684a283fb77ab3aca22db26cd4dfa8a718c17f9f558706779bdc7f25b809bf52ccb598cbf9867dd4bfce9ea89393a1fef97c61d1ee0fe9a23a0713f486dff988249a4b1997ed80573a632402b03e82755f99ad2f1afc9ad9652deeb2293605d0ae688156428b0003c76b48a7cea74ba5d1ecebd6b72c7df7c279bba901f3e5cf1f7a8f6addc63ec473b3ffa368938aac90c1d8d23fdb116332da46d4930b279f0acd3846a5ee6500ebfc79f270eca5b473d8a177a1b2159842c47bd174c781edf8f9f8daf9faf9533e700c41c903cb5b7613c7a65dc83df5bf7a64cb1f36486fa72e1ebb098d5248321796ea7c89fd39c293d52fb040f006eadac8e61df464c04b7d07266843b6a49e9945ee158c55becf2af873dee7236954bd90aaa15adf7880aea11312c325120a00a044b13c067b506fb75d43ec09850bca322b865646444c2d3c58656bd8ce0d7f191ff848b7ac1a08b499c6d98662afbe056ae07a1cb3c56097bc66ffbe817727e695b1a48ccec89e9970b6e274057f54b9a1cc2b47c5fa8e21f13fece14955c16dabc260dbb5393e26b89d5c3390a9e64e21b3b006fbfb3fc04b300b43d1e719904392235bacfbb40207c5f300e0c0a88f1901ea7001ddf7386ecec6c57aaa3694f182f9faf9defafbfae3de343510032819760a95e952e8ec3d9d2d4fdd8968d36dc395c3cdab46e85c72d599b4dbf9f258eda546312bad7050c4b1388e24b42b3e990fc00321de5ab2866c1fa3f9695aecc7b9b5d2cba44d061a07f4aa80b83b8c31a8b9deb939c657cdcc3d502442da2e96712882dd0ac2ad34063e85f03f48032a92a908c0e03baa6d5ad770b937e3bd5e824f1bd9654ce9d737c6c5f332bdf881dadb8730e6c186c610988f0f6e80212279c414b467431dfe22eb4d6bd513bafb74e0a0cf14ce28db95b71e7ab2bf7d1ced50f3210b00b80f95d71a0139d199719042350887a941d713c440e83c3a90ce44e11511696e39ae15683537dd08cd6c60ee4ce663747799f3aaf5fafbfae2fbfcfbdab2d320a3664950dda9aa34f87884bc135d090b04ec2f5ea4878b9961f955882b7e30a7f34f332bb64eb9e8621f86ef52e9ea56d751fe3a6fc9636c53764d97b5fbc67180f7757417ae4fefdb8c9d58c7b10bed72a5ff3cd9eadc2e710f9c8c55ce09724d90b4839536c7c1cb3b4bc5b9de4139244f9ab4fbdd28c43a7d0d5ecf9aff476a322bcb65062f1316ecb1cf2dc5292d88f291a714863b85985d5921a9d72e13693e8667c7a6ac424aba038a6ab47aaaffa50d0876963744093304708dcee8f046af72ffbe70651cae569c64b372351a108abcda0b375c8fd73485d4f73f0d01558b6d937c66d30cbc5aac22cab31f27f59ef94e14fcd46e8e390015f61c1e55f9fbfcfbe6fcfdfcedda6ae6250c1101969752a2b59229652f39bc53e73dadc3d439cfd2d0b767b068ed695a4550a1f1c81ba0d972dc59604917f8b6b95a9237d4af493864ec8552b550210e0657766fd2cfefb07d4250d696590c69ba9af8d521f3ff06d057d81b9c4f3bc4444ffa54f4f69296112b7d49b85a4923029531369700d51f12f834feb086716f7ccdf46d37f82a2201fe2998e3a74e2f925b5683b9610118b1292006b2041921b3248d3d7a1add6dc21b397edbb40430870d91c310cbeaab9abad596dc32145a9af9ec8fa94ecd4c96aaf7d58e81f7b9e4ea7c7ce5f1603c2070b2e833365517a87f40bbc45631fbd833f5ad0a15519c8e56a3dd52cd2b92d14cc31f73d111defdfcfdfceafdfefd145b0ca14895aca4dfbf508cf0818e20a092f618fdeb76ab9e8ab0d713c35ebd3450c16619a33b672b1d630c4b44f400f4135d75d5ef783f7319dae07c8ab14ce487e98cbbca1b0d47acfe90729c3220c2a911337f873372b6bc0f52c4e22596b8a037cde70d07dd98d28378eb0819e72a8669acc2ea77ea3b52d7b59d11e9961da70ebe0a3f4d40ffb858330b87194738e220ecfefe032083b2ed717f2595477b9506e429d5398e9d53403c2042272a573ae5ad3f56bb22cf9d9f88e03facbf8ed205b86c74e1004886611a853b4e8b3d95f5d508c8342aeb409966015c4a1f464fa35c81d3ceaee49b5255d418cfceb051d37bcb1566d7dc2113fd8b5583a8fb0c9bd33b8183fdfefdeefefffed1904a5e13fab13032318edd1c6a885f0a9ca0a40a6237105973879757dbbecf38fce3aaec5334a80ddf1042505f4793c69eb0c9a36edba2369394de1f3d86ed66a61e0f6464a40839b7e528d0d9bc2cad102f57bcc94e34ada64a8cda04efc5055543a80cdf68ab444a0b25dc309ffb0fa3a97524e74ea7b7fcbf1b489221596f045205c04916c99706412307b4baa5a94b8bb90d949e709fe2acb7bc392103d183286539fec73eb101a3485e584effe7b9a83f2eb4f53c38ab79c4532bd497be43961a567b56c0711e1dc59ccb558535848379b3460d5cb6e57102a9a23c1972808895544b3e8d02354c05f0191744cdb8e3a512dd3fc69e0aa13d28bbf71b3f4cd856878583fefffef2ff2a94190118fff9c8066c4d853c0ff2fe6edd6c2482ed73f8f6168f782cc14f6729a79f9d4dc5373a34393147f318c7520ae69ef809875398f91d0683a2ee758ed00802b324db358b7d02af3b77665c86831bb38def3bd55e982dbe1b7fb2a7b127fd076fbe5a961d8f5f0f062864a89331ee5c09e98cc3da32f38d035bff46b42404ff7fffd2aca17ce8b5235c7137cf468ff3c62e7b0b27f0783b4de66dbfc970510b81ea88c02d5d85a50aba058e5ff4061e5754969ad5a3acd873ce820b3ded82196781c9b6f0d44d23e23d43fbb0939b0e17fccfb85b1aec8cde1460cc5c73b02d8b5ada83e1410ced57c32a9ce4a2ba03b127c654c64af12df3322958e277c308b6342be0c54e32ad1e11a4480b021b8983a9f6ab010012912b01f50f00a401190100df1023c43b531bb9444f523dbea281a269fb78089f597c8b537c9d2d11bae989349aacfb8a3e9c01a5726f338e4ec3c09b7421357a08136e45c1288aa0968007d40556148c2ceefb40932e888b786cba3d6ef32b235a208bf2beea11bffb239d857728529cf68bdc3a3f14d2f22796ea576e38d62de60b8891c66e8f37279f03cc0e1f96c143a23e22ac359ccd4a32dd7ab761f8f3791a25860c17923a326dd88c2c9dff88c6ae6ea846313d71064fedfeced0f4a8e3cd580d0e8e6f3c7c033d1c7122a709468a95b9d788f507a2cb15f1ad4f60ab7b767150c37645f42c89f937d84e9d825cd7b0778ea38d3ec945ee639c123ebd3f87200b70847268af1e11a4c4e9207b8d83aafa01012b021901015b7e899aa3d1959c6c1658ce1be5fed4a8db289e79bdc9ddbd469aeb49cec44d4e85c3ad0d871c78d264833bc9a23a132b3aa0a5a1505b5192f853b362ff162eb6707dd2e89fb5b53b67b3e40c9236e22650d5d80ee756bf54ab155e53524d8f3e5ac5a6d04b02923dd958b2d116244f94a86f3009506f2ac5cee79005fdd4a40fd09ab27b6aba756c0fc10285e024a947add9605673171300c369656eb7c42fdb189c8ab1655271a368e0831738be61c3c49f701508082d3c925157c514c01e471b3cdfe821e20bec33f6e2c82bd58570a45b6fbf92a35f031997a4e5ae4e33b08a8a33576774d037950473ed673bb0b473179d39c3be75f3379c43e01f6605006a1baf1e11a4068ea2b49183a9fe020302bcf067ef05264c969ffdb98a72363f2525aa08a8f5014236a18fc0e2ab569dd434db8dff9d1d3a82e8ab32206569069e5ee4495ee93a0ab2773d1b243471c1830da63405901abbde785d94aecfd7793906c26d75c95ae3725b1d4ce5d5536b9c93a038a189f2c4b4adbcc849e2f866e981b67763dfeda83da87f74e1cd9f19f52eb3082e6ef6cf6699f75dc41600e1df22b9c14130de75a4f5ce5097f7f503ccc282f67b37f92be8f209a88ff54a2f1cfb176222a4a798a43782b042ac20710f1f5dfdc9b82f3500acb04e2de79e6135b7b862360eac0235deae80155c813ea00c2629ef360ecccb03b5267acf72cdbcfac4234e5e7084825fa75b1ed46482961aaf1e11a4a389b14c9583aa8274ab0103040355093c18961167646e22bc3ddbaf290ea5ca0aeb76a890b19f283c2287dc902c426ffeea6c3fc7d9abbf62c889ac6919d3d825c64b3c7dc9db1c9edd3a10e583aaa6548985748ec30d0552d20fe3c6794cd173a70e7d8f03ff3887f792e8d49c81d2d1063fea4358571a2dfd5d180b148b9fb907fd4542b4a6c0d1b6884a0a942f05056268c151148a782fa8beba4068d9a1fd0c5a7dcf62744b2afcbcc0f1d7ad127392eb2734ca44761c6e8636f28dbad2024f35696efd942a2d527fa9962d57cc6c3fd534bd80dd07f17b0820beeca310a8f6038a26a392390a1cf130db6415ca94d354572f1f4622f0584aeca36644a90871bf57791e298c64c9587180413d7a7f0b9903040386ae0412940d1000f40504e2a1b1c048f89a4b1c3ff3c37f05733c4fa41599a9d500fe03912041ac00120877b341fec8b4f5bfb5fb5dbdfd41a889885fd715c2924616b24879e7f7d490daa84ae04ab97b7861f4792a62a78636d12765480f3d150c76f899d4314675baddb8c5033c32b5ef70cfb391994b8a317486d00983a1e2f1b45a5950f7554f58f5416c69e61ccc6a58700bf7ef84d8038281e41acf939e6a39a095c1d29ed4ff8f5369675860aba58b00c22bace8911b6fb01462e47e898b6dc26ee91a2c7e634dd2d052b1171088e37747f6f33b18d8304b35041712b2e84fcfe7d68ddd731318055cd2742ce66b087c0c77031318f4ba34214cb31e9cd4687da1d591c50b2216add2ad009e0400ca837b9b9b2be6c78e78f5882005b2d17ef31a0100209dad8aac051292fd060560ad42976f5c92519e3cd6760840b84f743775238dcc33260bd0be59ddc02746afed2f63f88df2bc31d3346a060be07b9a7182a67b565c23ff4c981728ed7c018a25533509fc36bb191385ee2f169acf7db9e718272f33e5f9e7fd7857d373a67a029e0cfce912b3d89feed256d1895d861499bbce5d4db919394840d5e3f62d2f88b7a22fb98fca5f09e75581d0ceb409b0368352b3eb92e73d5b22b7f070c5a5612e8d34000e6f03739dd8c7167766ca005eb198423fdfca41a7f91071d1d750523fcf0c8cc804875c4e1a70a3b7f0aba5860d9f40a726646859d0c4c5dec366748277b5ede9a6315e20dc3c59861b05bcef71b4b8d31a40c02b509dc3142c5eae52a680a1ab8e0607063db91e9aedf2ce1f91585a946f5f7e98bcc616dc3f0bcdeb246c873d807c5e9638beb67135738ee14a3b972c5b55991e7ca1dcb727340e3cb623f01f12738be6769527124af6bd8d2263a0385e3b14603541c995ad8cc5f44d8a1152822bbed2431fba530902a6ede044fd8955d0ee6f17ade850bba00acdd5b14583ea85e88c85c52b71b0a7508c4784b56dbcaabc378eecb8b63664384ab4a0cf605fe668b2dba476edb5a3b1dd151a31d9708257ad2221d40ad018f7d9cea4c0f7748907b497f4e5da5200182c5e2fe8c839d85bd76397d97576a0d21e5ee44e855fb308f543d0353b844347a4490a93c972ee8ee2fba707120ece6687137109ddd5472b43e5af1e11a4d0314421a5aa9207080709e7aa2f192fdbb6eec4bb8123e6acd7d39503121610f926bc8ea573192ee4fe3836fbb36501fc681eb8cc827de37dccc30cc07882edd465c02b6598722a89aec09530e80a2415571248500e6351424fb9f48f83d0d927765ced64fde2cb96fcd4fbfe84438636bcec5dd848c21679e099a71d931f3c099ac8a03fa354d1dd6986690c1fbc072ff9e16d805acd1b130498f611d54f33e5abb475c4fda27b2b4d638f80d736e21bd721c70601b18a2f00fffa04d99095c91313af53fe82f455109ac1e8540731ae96880dfd021e584988808083d5b377f3e1d0fa9fe462cdf0a978ec8f93b853a6256410cbe5d26fe0c521bbda60660947b7d9f26807e7648f3e204782447fa907080796080908b87fa3c9d5e9a4f83101f20b97b3fc65a1fd9c30e4511629415bbbcd8c03bbaf3318b530f360049919a57c3f1ba879dc9a64f3c67c9fd130b4a2048e8486493ef1abaf4266e0a3e92f3bbb6e15ebfe7a1286878950ea44501e3422a4ac34d6af5fd782e31c8248dab221d166aba7a93043a365683b30731855f12b8192becf978b010a27212a8d26a415e02c451ec5895a8ab6d8b9896bbbfd4ac435c6577b0a2da1e8b109107c5554b79389aa5ff9a43be5f3fb34ddc80385f9779629815a73b9fdbc1dbf3aebabbc55c5fb9e3ca41ee8ac9ca212e19e9dcada16ee92ef73b1c70282763cd624e1b3997f6021f3dd5f5e543561fef1c2a3954a012f1832e5e2bfa3cd363cadaa9a090a0981d51d440a9d5bbe84c2b2b5fd873e0189d3871e7997bf52d864871498cd8c2334c9c2eec088b7164fe871cf8dc365db22115d25a50830d44bb8cc64447ac0d2cea7b5374ec3479af7215af07b0bb6d2ee5b3ac5a6f5c51ce462aa7286149b0446c54ecabdf874656eb3fe3c442e88a07ba0ab61678659347c23c81214742dfc9a6d42728895e2f0eaf674f348376cacf65247c0a83f25741a07ab813d8361b76e8fae7137ce6a845093f97fae6ba169fac1791e242aaef40c588afed24803735a77a22d05c8ee2810c047f963b9c3fd768a61b4efcbd00b8a67dc146142e0d9a28d82b63c19236dbc673db46e306fce8532a452ecbec00d751f0d910e2335a1b65eeb834ffeb183ab9e0a0b0a8821d91bcd9bad37a9d557c9ec0436c3f6e36af178124bde09f8f02191da055c368e46dd85eb8df84e221a461594a6833ce8601de1e7b8d31c2f0d68a4e0bc381597935d7b303e4a66e9bbd95699352e7ddfba5ad337f35c06db622c77514971ecb9407829341dbd514e92c780f56c7c7e893a7e9143e42fd04a69e7877818933053e91c3d2fa78bac3f5a7302f2333d0337c398f3c4c34d8a55fdfade5017ff16ab7dd0f9dad5f9adde43bcead7252d58477a6096d814afa8c77be5a763d639732c662e814107b6d7cdffa8d6298cb1008f4f891f67ebada64c6bf7b7e6174f86d79952ca2e5a650a1a31cf3c8444f530f67923b990ea2a911973303c43d9f1ae1f3b9e93b50a0b0aa20b0c0bba63162cd9941e8b4cd8756455e4296d1d292d0f3c439028ebdc62af22021acc5cb17ffdf412a4fc70f4471a5d0856722836afcae5ad36e692974da2f1db6690b3d3a6cd2d94d4ba6df41933f66fda9394d84b5b0449b485ad88b1575f3a277ca98ea337a4ccc95c4b244e22f44e5971a6bb9fc55eac09c853c73c316643fdee09df1d8816006b72e71cc533dc57927c85b0f0f283917be4b7c9b1d4a0ffb24cb6a6fed0acb7997537d9051f2cdb2728f318a922fadd17636f10c53dc5fa7da0d12cf6354eb5f4a3e2952d03aac3149a8a1ed34b07211c4baecc4c31ce657c07ca880208653195dc453cc71a24dc409d2d41987af0dde16ec93cc9f3900ba727af1e11a4c591cd55b983aaa60c0d0c1f7b54a27a9ec4f995c296d166bd46f56961f6f5954d09dec6bc356d26c4661d3475067a1dc6d98f8955ebb6fec809c36f8caba63c4441824e37db792c8d8607b35c59062cc1674ee7e83636a95f92c96ff92572aa0f5e95cf1fa2b487d185d25ade8b1557c2dfbe16ce6460f2b6587887e9c8aa9a85e628114c24ed106ef7a8b070ae7c0dd530be7866c115405efad3d6dc71c56a9be76b6e2f87a907c9d8e5aaa4d08246de53d31d92902fe78e93f0ecde3e1956c0cb9477011e39cae5b775d59cc4991f3064f1e0662f45811fa637a5f54aa6607a2b10d65c7a68c69a5f69c3a4eeb9f6ef5d356cfb6b832391f5b96db87f67b6a55a3d8cce7816369181656f7fe1eebd0c0d0caa0d0e0d68aa6bb69da07c1205ba661629f570f8487ce8c99f8014661a791ac0afe11400e29acdef8d5d2a5e9542f81cdb9e0d59073fbac36e118121a732efac9b9a4e2ca49f399827bb86059880d85869830deca3d7ff840e7c68adf71b6431654fa65406726a3892b35d2eca81a0a20b4115bd9651216a8a45514d56ad60b6fd13c4b4548c18114327502ff61ebe4cf8dbeaa1b38be13c2f88a789b50f9e312bcc19658deaa9470d1c273266b6ca01ce437c9a717a26d6057e79431a35010ca1ea2af16bde58b9e1dd7561ade0ac1ae1b5316c963560a4282b17ff2345756e7f47b5877fa451129720ecf874c27ec3b1e1108b8a4c4c485c50f53bdf01fefa15e3936ee80a0294c10d0e0dae0e0f0ec59decaf49bad7e7fbb3d41f398f49186208d41b17c5f08412f6da1878d94509359e3bca68fb78d4533c548a4d65b8935b4690ed695ea9f57aee2fc873bd941aaf8419aea30524bc728df34e79f3d2e49e9afab598d720ba1305f6eb14b24a8c85c1a7849417ca35c60f7126777fd478b58dab845ba933d7cccf9ebeb2feb142bd033c5bddf11e21bec0b13af75a79bbe487bea0230e1f5c85ffb65420912feaa750be836e7c959b011a558be946b1377da82f856069843f3bc1e906d84a201139f1c129544882db6f8c110fb2cfc53283f9c0fce7919a65106a043397cd072f6ecc605ffa065dffa2551982595484fc5ed0609f65c2d9b3085d3bb2f99542283e0734bac50e0f0eb20f100f8cbb4175b0ff16ac25a25a8db3de058f0342ac678209154674b0a4b1f86e39f2a6eaeb660a2822462d0f06e12e88db3bb13461dc5aea6f7710ee46d33c9d5f9b95823259893b103a83a526a8ab42e7a4525452458406a08c42296a5c00b84363c8cf141c1dfc7f77c6d28fb99956592d90ed5c8e9f280ca07abc3ec7a9bd44c8836dbbfdfc5b7652d2718dc3fe760418a817fcbe6647c2704fd4c8e3ec21b601b64c567cb0b78bce5d9297a6c3268be84e5e5b310ba90a96c6c3ed1b8ac15ef026460d72ed761ba1a791678e60e000d3afe3c50e5a5987189c9fe67de0a406f0809e24eed1b7e5b002e7fdbe6d64d87eec6b5a5834576c232bec0a0ad060e1c0c297fe02c90f100fb6101110ebb6211ba03b6b9df91ad861da6eace6538e4c485e40f01bf75b69f15ced3795ad40971c7e4d54625de8bafa7335b54b4ad4e032ad26e5baed830a8e46222a4ba29fbb13dafc1868bf31dd316464dff84e0feb27925b62de73b6cf22b587bfb8ff492c55865839d3549d7fbb295025bdb72fc3364535afcea41ff4a29caba555d452dce6ed39b77064721dbf192f34f48bc856049d813f3e88a2549a360867e382c099ef15e90e60db55cb76504140fb03bcc81f6d43013e6b20e6aa4c5baa55af3c619a76765418b014deda47cee6a6924ebdaaf6727760ea0b5c2eaf113128441c8c774b7b33e8259be7baaddf6517e089093d37cf286eed6977a248c6256a9a629012cd101110ba111211b73c164767a4054bf1a8b050c8458f91b6b62cb158b98af48f796a2a899e9a580b87c6a07b586565adeda552cad739d30ea4b19bd06edee215943ecf21906d898f84aeda193d0ece0c6f9f6a1282509760d68c57ae6d26f1b346387614a8dbddc522772e10bafcc227a6a9ed545e38acb358fb9feae86bbf6737d6ef9c8bad38d9ed6802ef0b23c4c8e6350000e5bc5f9999d91ca7a838cdba314f46637a8e788765a575b7b2b878a541bb79ec23471dc15cb5418746ac395341c40874d9c5f4e6c3a18f25dff5a5f88bfbd52ff674928b11bf9cc109c02a0f39d5d4632d91c233e879663e3b2fe948bca55548cad18062eac14a3795abc84b0517c7ef27cd7e5055edc3d1111211be121312dd06603d6554e2435db1320e4226338855e11536c4eb66282bf243ad25ecb3866f5bcee23a8630089f27abddfe2b9747a00029bab2f2180969df3b0fb97a7da1a21a713e74289729df0db1ba06a82b0d7bc3682278ff54f1f92ae826294925d8748e051351cc85b18742fda10cb056e88f108d5886af620bebc7e4c368c7306bb900a075a757f2389ff4ac70407e23cd7e3708d890ac07d154dd09b059946dc5b59526ea232a4c24a16089a7e05f0f8fd906ec779e601953fe29f577bb65c7b19b053076ea655f6704a981a53d9b54e292cff0a0548c9d0962a2fae828b201f2cd95b90647dae4469c4792742f61b7fb39b6b1f7085d37394a6d8984c79f22b5b8ee58d5121312c2131413c72ecaa564adba80c729ccba2634e619cabc427a582d0d6c80ee686968c7f38fb30746026aecb03c46bd7e69ddef9a98f8e794eba6e32b55e00ff9977341317a85e89996be3f0a9645ca7e42860c3f5d1663697e83fc15e385f42592e45a5e672b98be3a273b0c212c3084054f052c29a420a72688c86d72e3be119bb2dc6f66cf44a8b47579882ea4d81fe22f20d89053152801d65c15148d091d88be19ad2cb6c47601e0ea85685e46e62b3317e90e0798418f71c92733dcb99f0ac9d7149630747247b2df577263af758c12cc3c6886bdeb5297b6b4ab3fe1d2da659e876ac2635436bbfad063b13d4f45179109b73563b0b591bd137f48da48d5eb1cfdaf1e11a40827cf12d9131413c61415145b02ed0db18e0ac69d5ed4594af876670c6d45ae9f12a6ce75580f36c303cba8e53fb7478a10583f9bcea6c27d2aa32ae0e28776c1cee0e957935ce0b6a790a897ff2ea9a8227b0a205c20e5d60ef1e591e9353c0435a0dcbc03d7c37f08224d76d05554aea514e77d4e0ee9a70e1017805faef38a089125710c1e9d1482e42c8a407d9b783da057369ddd319dce38acaba5c33f4219aa8cdff3a02b72447ab7901fc598e7a08a48cff657c5baa8fb56c7e24fc0bd6e31d4ceb73381d43d35f3bba4b4b08a86f2dae884859e9814d724b07f516ee389ca8f47eb42cdcb0e5a98fdc5c5dfb9ec609cf4416c1251a418ec7ce71ff6c9e59975b8e483f05dda268faf1e11a430237296dd83aaca1516150fa3b8049c9d60d304f82c4f77836ef3c19e400a3063e8010d904eed356cbd0577305bbfd298dea9dde91d8fa994e638e00950ac342ab27d076822c0cf005938aa675e21fc6a776c90e48748b5a810a84121b86cab00cd979788bda115899fc965ff81bb434a04b63d969e8cb5a8978a95a3071be0d8ba0dc9178ee054dd7cb2e357ba62514202deff62b1753c0b0343443d5fe95bef88156ec2c3ee963718fc82698e2c0d6e1835ae670daa94f5b2026078915b08a74035a5102de4437c40cba4566e6b1e8d53ea383664107cc33a9d8d3bf0fa3c43eb56ae7c5310e2ab4e8c3a9989001f03dd751a9fc995caf68ddc7198e15cabda529a148adf208baeab3857c55ae9e1151615ce16171640328e58ab92d9e60a9ded43b3819ae95a9b8e76b62da8c23c4fef3ef2a5116832c3f14843e7143e42ab38db2988921aa3d5743fd4259086ef2d54a70b50dfb127b47483a4b9af3ae9904d72cfaf0bfb7173526a9a9d8ad564f44e6da02ca222bee027298dea21defebae893e6f7a5823c957968aba7c430a6534a84506575b0785693fd9afa0edcf52f7ec2c78a16c806c7c0d7dc78aff81b79abdaf1bbb045bab21ffee38c3da7fccc647786979320ff941bee3097e6a25f6344a305b580fbb9de142989c52b5837899278c96a8d1942b0a11ef3825c039727ea1f317dee77051a77b1617633a478227bf39a5acb0fe7eeb11e262ffaee5e15698eb8736d8c7827686c4be5161716d2171817b32b79ae98b3573b37f1be930015671b1dd7a21d4d1dfa3e136fb5a19be8ebe03516e75250d4074211589cde48d9908498c594a52b7d1ecc292ebc368a2e75f14ea6050e0450dea2d3ba07149c398dd95ab3b93c1a1ab23f9328d19ac844a43bc24960cff27d41dc9499e09f65d8206219a24761d816660888dd9eea2668aed010115625e233dfa077997a53fc381cda174389818934d65c42af9baf883b972ae680320cb7434f87a2da023ad883e289b60fe4efac509c56e041c628f77b38123512695a6a39a64908de0b42de715313ba8d2209092cfeaaac4fef1ade80b0ba6c07baef753398575d0b53e8c136c30a2488a91820562492d370ac9a8a2050c9c74b9109eee9171817d618191881cfbeccd154e07e8177c3d70c53b7d2c250cab0a0b417487dfd3b950ca4dc66c6462752b429ef179022f9540a496ddcc628c43f38ffa6f68fd0ead56ae359869628eaa003735971b75e73b8e7255fbb74f81b47831fb1c5b918a94585d87142c6a8c4cd16ca8da93ee6ed4de9a3e04283c7a5980e3e89dd8f36907a2ecdaacdc3e4a2a947eec895ea1634f89ce7fde24edc506571c150408ffdc0a5cd9e6119b17a0c9598cf15ebd6cd59ecb45dcc00051214889b78e678358441e74090c20b83c1ab7e503c991ebb056296c15ae32a95e6e75b11f37e830258deb497b64a1f60f730cb57ec0f177675e933dc283315ac52fbb563a7bb0969b34505c0fa50db509c0cb5ed18191883f9a802c21170022020c20782f0a207c20d5f524663031220212086c41880423080a022310201a1704cd00a0a428a60f106ad31d6a0934bd8c13e709790972d0aad2d9715924e9dbd708e730c6720a903ece0d6672e962085802d1014040db789a451937601dc67196e2029c7cda250120f7618be2db683f358ae214395ad5c53f84d0264525c7ce7186b5292f38c607f70d501d773612d2997014121377bb15c2fc4689fe76c83c118e092a321da7e4b10513889f1621fe718f2e88f785bb88288e9cfb2887680c4076e0fea042ada0aca95e11dda955e2cd739c69b64ea12f8c1662d51511e0582ad6041955c6f042541f2c279ce31dc8ca4eed980bbc12dc275b28d5c2d409aa0ec05f29c6398914edc3182d7f742a447a1c06644541fd71f8022277b215ce7186f12a9cbace0747261d2a34140334483f327fcc635c339da915e08d739c69b44ea322b383d9e0bdbc13cac8e06175780776c21bd70ae730c3791944b6e3377c470f0903017bf4f0108c806df851ddeeac36d48ca5e2cd739c69b64ea122bd88e17253d1a04344324d873e21fae19eed192f402b95e88a13dcfb3058362fb1e010bf2c17be21930162e1b824a227be15ce7186e222997ace0d6d95c28411a043443241834ee1e984254de8572c720a3755e26400f14e4c074066a552faa824cf46a1620d8f7c0745af22e949b8518adff123197e87b30e3204840c06cf8aa63917d0f442136ef42f9cf32d6a424e798a07d70d5715d6a3945904754bc717217c9ed428cad01bf0cb35b50004604f71dab1c8c38296e5988856b88e4e870c320a375e0c73c56b6a0188c588d2afa8b21b01cabf341bb15e01d41d3b90c85a5ef4f06c1ce40a3b5fe7c5ed400b71c8558e3968858ed1c0495cebf17073bc790ae4674ca08ee0f6e1d74dd0b6cf9b60c08ea9228d1f400b6c12485176268cdcb0980413e16cdeb730416cc87b78433b61692824cbe0570b700436b5e4c20001edbfc17201f0d51b2603ba2f344b605e0a0f2a2fe8c2f58de222fbb10636bb6c605fe8878e830e275891764f8a0d1a34db4d35605624917b7e144c3ff1dc445c0ae00438bdf0457318804b87cdceb0bb5d336e169c9f0d88f21e98908b0d82ab318d4f9853e21e20a6e5aaf503a28182e861c35028a71a59b73e61e8830b3ce42e56718142f83cc7999bc271618ab141786a014179df197fcd99ac7d9cc2ad6e389ce2f3836876846c133b81f9194db96483d5b08420d7cb5f091f30b22ecc28f11dcfa120749e40e059a214af81304fed9caf0cee8a885579d5fb8753797cd9a2bd24186dfe60e6cacd1161249582ab458c1f905457b460ba5604770d7915c6db455895f2c155aace0fc82a23d8e620a1e82fb1da91ca130116d010825f4d5c287ce2f9832154f5670ebf1afd80ece63390712d5cc686b4341264d2dd6747e917d8c452aa6809fd2414470bdc3d59ab622c1c452a1050ace2f10ba6351a6e04370dfa13c27a62d00a1807bb5d8a1f30ba766e58915ec0ecbb6a5398380cd8818c78fe3475b19de11296ab155e7176fefb5134a6653efc1c340a8823d1108c8c6f155b5d5a12073a7166b3abfc83ec6229982ed625b821a19018bc1ae6cb425862048d25aace9fc22fb188b54a6602c74f8f01cfc1c5e97f885516db5018949550b149d5f607c46224dc17b69d0478d1a01c5d0abaab6348412e26ae143e7174c998a6767051b549469a825428980cd88386c4f8cafb666b82752d402abce2fdcbe6b4d48bf03c1956312abda12114bc4a1850f9c5f10616b5eace076b440a9511090191f55b5f50720e6542d44747ec1f118453605e7a2055123464031ac6e6b0673696b0c4190a4b558d3f945f6f162b1886c9b4a2c5a3a28632c4e615c1801c580d7fe0a1e575b33dc13296a8155e7176edfcde56099393a8830e870411c8080d9085f5d5b3b0834676a21a6f38bec43b4157316cc2b5cd134cbc1f340ec78de67aead0d059d9e5ab8e9fc427a11ad8a59704770eba0d7bab64a54743d68e186f30be1645c549b9a85d241062e4a073f87d78533b6169260a6ce02e46718085b8398770d9857847648382a2c108256f8e14a5bef62432a0a3ddce35f0b029c5fa43e47afad4cd9a04a0271e07a82a7d8690b09ba9714b5d88af38b6bee7d003bd807ee122e036bb56585a4adab162e38bfc0909981452fa1393ac8105c7c5cd759a308f088825db0ae68ef3bb2a57c86a1d452dc3341bb937309053623323c7fdc1e9060a0ce0264e717085b837043f03db8efa00887a08caeb300e9190642f608e65da6a9130bc45d42e825588a854dcdf3ec8940fa2830c8676de1d0f945534de1cd14ecae165835eb40d08a1354b4f54610cc162d56727ec1f139d75c19b0e9529c528f1008b2ac80ffffc39db6b4ef6a0b5b6d7f3fa425028cdf03da1912911a190a6dffd4215b6fe426a574e98eab6777f68385d3caa4852225e2ed5fc396778d38cedf2a1733d17dc699f5ed828d9ec191d3be0b24d5621f96eb35cd8aab3e3e5a8fea3138daf069fce80a77a6b0ca416a3f762613144c5f646fdcbb0759945d49785c78909c2d54c14ed87ada5b17c924b26e5fff3c519b2526fc1cbd44db1a10159f266377cef84d58b35ac412b25e8c8e50d2aa2b6dd06909141dff234e53e3a8ebd5230d832519614702ff7aeb64b2750b431eb26cb3795e58d169227c6c783c6e52b93b72c486db41a0a95e3b9b32afecec1388e743bb6d4f745bdaa4ef0294996e4c0b17563312a1450b5283c857dd12fb0702318c69cbdd7945293d6937fbf571d8f1191a19de1a1b1a26bd68ffdbd09f193acb807848f4031d90017b4a00ffaa8a4a78bfbff99b70f95cc681ae64f6aaa82020257b51d52f867447f90183c532876bb5fc3b8d2343b4914d9206f3f929d64022ea6e0ec7f7922d311237f7c271c4d31fe450ed3082480ecc28c5cae1c440b908cdbf1e2d2126aa2140a85ce24771a30a4e4faac5af694bc8b58220020a8f64f226b5981d38ec584c5453d77a9c3be799a61622209dbcb654066d43cea81486ea9f995624af5f3f8ffb7df37717a85ffecb4ec62a0a2aa09e9fc6c0fa46a11f84dad3e3b7f934b50befc13f3532dca465f499c7bbaba4a20864a6fac83585b956e5852b28e95dcf23282272ea557f5380263069a268a5af1e11a4c7b15675f583aae21b1c1b0355316b29a3c9851b8261746ce08749b815df676ac249e25cfdf7f60223e55b754d622d32045d926f44965c09329c5124ea522d9916ba8fff8b1b99101cab37aef46a09ef5cef7506467821a6f173a96a360e28c7579be8d35dae8da7da1bf95011c4cb341725a51f48f4b7df81c448b31ef814130daabb5e5658c7029a79035b2dfa41d73050e5869501bcc673869a89a630627d4c8c8a4e536411e97e4417904cdd8dbb4a0b9fc6b048e6ed9c6a900df2df93e36c20a001f7d118079e61e144500ae2978e1784a9504993643a43d08250d60ea55765aaf81b1e53c9985fa188fd3be3a57ce04505b2d08ad5a81c1a23dbc3f0d4eabc2faab5b610da0d5dc89332867af91b1c1be61c1d1cc570491ebd448cf87843c66df7cb2985d23d86c19b8d9b0cb776f71ce6d1db47c74bb8e8a3ed732c8a8dcc122eba2d6e3ae06e7440a7d54f8f684432aa9df1a38c16bdf5dd96fae8dbca502fc0cb8239f3e72b41d390bee8e8488f8ff7870089fcaf3fde94cace2689b84f6bb387b140abb0c31dff10420f1ce95b3ccb89d2951fe80756c1f2c765305985241325e383aa98e06227d456a4e5bd3390c4f907f884aaa3e5f9e1a12a8ce4e2d709d58070e0a7a22d1234b73708152debbaeb69a5e99ad6b96c7dcea804d3e24f613e9e13b81208952230d865650e866d6cb174304a6a332013eeb5d4d204abfbf1282b3b84ef28cd62b24f69d48340d2da5bf6d3fd3679f8fd1c1d1cea1d1e1d209473b8d2f96d50eb1bf72d8e4af42b3cf875d05f1868242667b50a1cfcc10c349fa16cc6b3a7ab01b39b794d31307a924bcdd9d609ffab4bb5f28e9a41505424af9c0d797e51548e9eda2d4432b36e5df68451201934cd1fceac3824cc6b06d078713acb0231100fb8963e39bc6c59fcb2b5ceef6106be142d3e716340533531b56c260b774e637ca85b27d1ef593c7d26204bd060da2d10920f7f3c59ba5f1cb685a6ca9fdf8576254154cb0f9781c708471d12c5de236c0422ea267b2f2455e1349a2b0eee8b51acfe5eaa86fe89b8995ab74db0875a09c55da899cad750fa0b7c4a0e7923fab5413c8cd4b3ac14d8209d6cdbc15d33bb7f70ee2807793b03cbe6c83581841d1e1dee1e1f1e5c2357a8baa65762b09aba12c3de47f6e5df1fd292320e28119b5462b0770befa9ed204106532f93c46a0ddc10a80cdd965894e4550dfd7ded8a23bea01b2ddb802263edf7f48297ea42443208d4628e81b49ee94617f02c44f27994d7976c23753fb70d1ce1926db7dd71d449e8ad70b37a08160b27431782021fc6e461cf8965b3c3f69d37ae041a4e91368adddccef58345ec56156ee3a821f8a726b41b53b0987fb300045d0f351e0d1937aac233e226744c02929816a9ba9e787ede666fe686748a1f0c427d092d3af804d9ce4f9442c95865e32a2929c12f16ec309be0bf5c692c069a8e195fa053d0ce5fdda0136a61d806a0e2b812b5059a8ae77384ea6a11d485841e1f1ef21f201f1c290e299db20a0702f8825b30ef77a9cedc28cd33c44701dd8f12559da80bbe7df69db5ff67abdb001b0cad9a44418850948fa56a46299df5a5c56a34484653988a0e1c916a55184c74ec5047be8f52c587b9e5c759076703b6aa9af7b3f24fe851c940ac14eab1d4a739d7ae42f79a8ab388bd05808af119722eb2c3bb466c094628fc5b809afa727647b05bf250b17e88b0cca4b077d21215b35a4ef6da938be8677dc53703b9b00c6e1ba21fbbfd35d4cf1eecb36767c41e8af99f27d27fb49ec6f763d95ba6e1ebd3ce94dfe8d59327821df7c6425802bddaba95d7168692d425332412fb47d07fc10523f145d33df051888892e913fe5d900356645e4a6ed8753889841f201ff62021206b5651db137039b4863fa8281b4b6f3538c1f0bf1e905a5c0e05e8662b34f20539fdf5398e004eee0832f482d9e4575d9839f2ca42856c735a064c5d25c7df7207913149b2a788b32da7b679e5c903f55a807f907d105ca06b2f7383c583d37813c5991dd3bedece5513e1dccdbd46a99b829d288a786f4c7d0c716b24064a38b3f6728e9b974453872f9d6fa901919390d177a92a4aae5098fa151c737bf7e09bb3b351485714b2d1bdc0b48d44a4a021cc16f7ccb1d8afbecf27f20f7322016b470c3e151c3d1322c6240b274d7e3b05984d9b536af015263e4bc13308ba71f11817a281fe2a425179f3ec49dbf163d1a2790f673ef1f5b2875e4ae83dc3ebaf1e11a4cbf12db98daafa21222112e9de2466a59a2d740b89f699b9b71c491c12f92dc5ec595f88801343365ad27778f36cf21767e490d1f852adce8e912fdcecdcefad1b271e6217251acf89e8a25b7ed9c22330e599e3feac5734492c974fd31dfe25114ed84d68d686a6741c038959587c4f6cb634be773fd73a28e5a523f711df70ca445f8cc8159504bb5b2cfed30055634fa84b17d44fc6f41425124ebd62bfd056154324d3b356540b78a90c6000f280bdbc08225ca62f79f2593c581026074982ab43e5b1a94633562cc54e966619f9a15b50ebf8f4480d057a81e65a9014f91136cf7694dc2e45fd4b785e8975d6af3ae80c283ec058f686e6345d209616cdf27bba944502a73743d63f9e2ddf9184212221fe22232275b83e5ba7546eba49295cbfeb44a9291a2993063e242c6d922094174a6e4e71c15235569bf5a8d9ca00c99ff417190bb3fd3be697c48630764dd8b72913f406a1ea4667a4dc3947461bc58a807272dc1dfe2471510733d1a0b4d34c594ee5d13c122ac823cd74fcd77bdd1365d6915f8373c3b4373737c8edca172c6ae72ae943e1026b714eeacc86b8652a0c705792681f268ddabd329373792520b26106b68dbfa72e3397d84d55432ba82a7d2c5ff80650b59b496ae3d63ea0f50c1bf625eef7680fe9c12031913b9cf8006c66c496c91b8f0668e43abc7ec67fbc1bbe120c9157217ba11e93d914b7c0a7194acba5b4ec76ce365a08eee4c0fb952d079d0bac15b895842223228275ab2324237c59c54bbc9aad8b01bcefd852e8f1b8389a78cabc637473f660dac1d5963e91340cc53cb7a3d415c478584fcd8d2ade42b0715edc8dede70240d2160a676d1dc38f4ec7a67430b7e42171e979b41545440971d2448fcded92342d5a004329641dbb991cf257476f4d099ddb1ca94f4bfbb36446fc804da0479fec605075447ce3f3f1cd8c5bc56de1650db12df31804b5c613f55aa13beb0711b9e7c44fba78f990be5b456008f7375464081572ee6c0baee5c03f99948c13db4be1401ac2435e05cce0ca5a03790d393ebed5a3c4ed17b59d178f006194a1d8c4205cbe09f2ef629065ae988566c638ed10efc741038f07491aedf42090ebb7aaeda6d622f13e0f0383cf998423242386242524eb04e445e211d89eca0f89959dfc6a886796a94947dac6907ce50502b28dd07eee5145bcc10bf095735746f36711642052c1288233f76c5cf85dcf5750b2b4f4a0c1f046e819f58742fa01c7c3f82f1545ea31f0ad9f6a491cc906631713d307103353b00708f56e032dbdc93ecc4c7b89f4f5347e67694ba88aee60b80ee64b44c03bcb3854626f0fc079760cbb5a30664d19235a654991ad9f525265de65b38c8525d6b1f83801838ad2a3382300d05802e7fec00120b6987e6e04266eab1edd85baa2554098557c344a92350b3c9aa5c9fe0f8ef7d858c766124405d72ed388636f4e8e4b4ecf49d56a0654a8f99acd70d04bd82505041d58860f5caa63b3935f1ead9d2425248a252625c84075a9423abc62338f59582aa7a9030a7ada79caf3e025dd4fcbab54c7d1fa2738d3dff80769d71b7ac2f817c78b08955137cdde88073c0f4272a5f2b0b43b904147b050d53392c61da2cab1aac46007e472fa3ae281aa965783d69529f9df3db461e48cbf57bd709d3764b0500b8d328724e4c0bef55dde818cde4b6181c90c5b6db1db44abeb9ee925aa37df2c5cc57a6f28b241588cb438f3b6b6f024b7623e1a02e259b695941222499a5003c4c778c812520651917f0bde5a52dcbbb678d2b2e74f4dd0d6b16aee875b6f6b8e1b6cf60a2d2caab042f7d4187b9e6c1158038563da0d46ca008db74fa7a2cf6f3ac90f8c252966ebbc8c32276cd7b28b8d7bc0a12526258e26272687df3d788375a12e8a1c084668e2c57e34577623342d476920e060b90ad88a7bd664c252b0590384bcb065bb8248fa3cdb0b4cf4776f31edbb4ac39915e4dfd694f755c9d4aee510f8cd5e7c284e58dbf896817452699d76f502bb87b7c74a95bde7d890feb66586d056c97f2f6958fc8ca677729f1cf306c052050943cafe8ac4295e4fa5fb3fd320e58f067935e6d57a2147e02cfce7075d75b56919801ba7b389f972b6451d41738e887ca743fa7bfede6ba39ff40cdaabb7b698e8f8bb3bedd710331d8e21fb39e6eaa2271d0ff6868e8693f6b621445452c87b8e18c07008fb6c6a46295c20caaf3974ead5e435b3783730ae5e8c51b0b6ed6f5de30819af1e11a4e9d1dfe2a584aa92272827ebd1d6479aa7e1dcea2330590b58c339cf443e66db223654807073ba1fdc815bf6423d840c217a693bb873f808376ae9248205ed74d665fe122fa33e930c34d7973f47963899bb3f79f9f118d57e1f10c7ba9ffe2e4ef3c4e9d8793ee8c2c6f5a57a88e929d4efbf68268e73cb31f8669719df7b1d6c115da1ac4fe8abce9c8e70820d04cffb28bbbac03e0454a2c0b37261fb0fd8ff4ea1a2d430787795ed8083f586272734c8cf9a1db2516b8ddddb22a985146569e17f799e0eb83ef2d35ad818e1698a3178c980661a3d6e852553a238818a6ace4d01739cc5ac4a29da61772e0f31437b5ebb285dff60968e669970ded6eab9b01061c24bec36986eecaf1e11a45291d73fa984aa96282928a0249b67e3d2a5305ebbddb5bb1f66a88ad644f764e35b33715c4c7298ed5cfa3531ead2f4444574caca572b4b6a637a9654ae5e1a1f8cacfc60134e884adf423afb8deed71e89d83cc493f0c733c8b2548822b14507eeaa471683956c5ffeebd7a049545d2bada34ef7a0564cd56a7f9e2e999c44f5db724079822b801f4b2ecb82563f1ce9b38bbe60e2e54ec14bf94283d8a6220879c7fbff2f42b156f4bd8462a002b7422079fbe1dfa330cf1d6e574d47eb3427bfef130685f06b0d4af67f858f7d96c9687e675a1eaf2f74924f3dd8a5db68b990f36dc95c420dc5a843a8c82e987a0780f8aab277e707407ce531d36242cf748449208a0e44747ddf5fe041919d045e842829289a292a2957758c4bbbab72b88d01ba4d6e2474b775c8b47a70a29effcd5e54714d04a822a7c50d267964f261276848239a48301b2fe62bf1906dcf720d53aeea803ae87ea7d275b1fa7bd877940cb3905c793f01cbb44bd78512c7ca109478e5e02a6724120a0c990553ada45432c75ec0d850a8a1bc6ea81e414c1b0e940e64c8997e411a858c118c440b854094b77bd60591dd04eedf4595d5bbd4489d747cb425ab0da254c3de701a65488820cda9745dfdf3b1c6dd931d703b8d4780a222b9dffbf1f85b2423b836731ac2e736c98b42bf97b6cc6c55ae191118484d018b7e3c38eed183bc02c4146d4bb9c9b4d861e86f0e273dbf208f63eaf588dbababbb1db1c4e0f13e76b184292a299e2a2b2ae1180e06a18fcbe96efb6ad8ce8187b6ccda62c87fb81aac2607637ac442ce7ea173ab3e01614daf06c5b6a4b9b7c0f1b55e017b56508848134b2f31ea4a805891f8af70f5ad96986faa247ef5eaea61b07264e3decd07bada5831dbb79a45059f9ebe53cabb3a7392fad6255e940f9e85b5b7bfb1c13d3809ebf46dba007913cc0d692d7cb06aac8cd3e4ab4e88650eeb0c218fd5b580f03104d5d8d4afe28689c8a4ee24cc6635b23f6a5b934e97a7807522781001ff676fa5ed65c8097910fd53bfcdb775907624f0a8ca627c9872a112b8f0cc2f7f0496e1b948487e5fe444285da1dba9140765918b19488cad938c933869bdbcae559857c3b3e2a84d2a4b406c67b52a2b2aa2ac2b2c2b800451121e36018ef3d974a87f0dba7bdac6b1975f6cb24dc82e1799cbf5a460383a35354fb583981794e3ff9c9323ceea4dd0bda9b199f4af1853c4bb8b5ad0a9f6b826856ca01236234f047645b1a369e2d800bafe9bab0d8dc7b2b5c95d4031c50ba08747da87ebdb4e32385646d5d4145682ae930b11d2bb9b70dfdd7d5b1dc688500eb9f1e52e851ed977e38e667c9385e7968b82706f47fe09aef166d725c57edcb873563f130842e1f349c5c08088669e64a7fc366c7984d85e70c84df6934997a27993427ab90aef5b0ce08244b86f2db6438c84c4b2a3f074fa6d7132573b4ec0f22c4910e0c801275319721505379720f72a5446b83f451d5b566f63171fd40b3c00ddb9842b2c2ba62c2d2c9c8a1891b37896e34e0906e1d43b15119e1de697d4d1452d5cb700be6411430d37cead0511bdb331bf197b0489b31e4ca0d8372306f6ff54c36379e23af308be8e851280a3dffde26c1693eca78d4743fa41dcfd5b52586a07e65fb702850ad8338f8f004ff41135e2842851606e3e9f2fb5432aa8b64241021333c356bd71373145d8a5ad0fcc46cdc65840e6d275e5b4e57102e03bee1dc0548e860c0303db17803697090d036e5177d0477c3350b93d19b44b99ec4cdc7329a4047d2b95e887104aa49c50a66d7c31bcff2d198416aea5fd84571211d8375687327509ab063aa76c481b4e858e2ebb7737d285af8902dfe871178dc22ccc55e654104393e61b575b565ebd842c2d2caa2d2e2dc3ee9e413e4467c1519464e160253f25ddb7f065c07b54fd59ca2db0eb449955214d7cae8d42b215870a0ecb56a551f4b3b0d31dc39d3c5726b490b352ea161fa395011188c549a30c38f1f4bc0ab6a645f1091053c989797831e89a2e14a87459eb806478e1d49136ce2825684e17c8ad5339f8f95c65c3f484e746a5d7a2a0ec832ad8ec74b63fd0ca4c6d2fe7573a5f61daa05e2e50f80a5f374fa7f98ad786568a42a93d657d5f93d7beef80fe3f091f817788cf85aa06702cf389a7754730b8a70e8da3c0c40729249680248d6796cccb09b83cc6b91541b068d8ee8e98b0480010c1b639d54bf890289cb034f7d0a05fa06acb64a7e5a49fc529311301004c7b92c12d2e2dae2e302ef8d24cdd0ca59b336ae22a78959360cd52ffcfa9c565de4ad2221f749fea0c063969b6d2556ba699e39ec226ed8a4c3e791b4b1da55f187e0a58a73c0c288bcf60987fe1c1979f8d49cfd73dbcbd33a471a5e22035f0bb8f544255757d5ac2ffa27cd4e42b2fadfdf491636964eb75535daf07def4aeb2a7ef7f6f6428250c3fa2f4fc5d13a2dbfae3a970c896d8b6e36b15e4d6a611754402a686907a5f1996edb629d93069c805b0b4cccf2ceb4e1cdc3c9e77052bb6187f2af410066ab0de6c9c4445459fd3d1f83a292394d0f4b076aa4c252d873621b9b074d0017006a1932fd9baabdae756944a6515909fcec219078ed131c20d227cdcd4a7bb2aa97ea0b75f5f31c52e302eb22f312f64be1799f09989fe21e0e27cc6001a3107139d3c8ec79a86fd4e79d81544fe383975c068fa041f12cbaac4925d5dc56c2ac9c3229e79ed553f98e33c41372a4d57a1f02a04f85ba58f8e73e13b6350d02664c7cde56a02a92019e4d9be9de059bb2851f51efd1c4d60bd348f7ee0d4f6f2b13d4e4bb0940d3ea15ba29d13524255ba88f9cd4175a662c1761538356ce0e79c9986f2809b6c78aedfd10546e433978edfd25981a364481060242f71e69ad0326bdb2ec89d46aa5e58c61bcb6016a9299e1c32412b4795e697fce347e74892822fdbd1ab39127f75d5a4e0a05560ab0a41addb40428faf9a30a75301c92029975675e1b632ae94876cb9723af521daa7526712c9842f312fb6303230775c7c964978e692eea2b857a2e2ed6f3e7b48a2330b5329d5dd9922b67b382dd8ff9d1bf9d956c24b2091524c4aca411f31be3e6e1a13347e21924053e3dad4a374820ecd98d295a4ddaa5ee9f509e8d592beafad55656bfb99a97f2283cee0dd2454d1ce33e8d8d1820b12d6246fe0af877589b96d4cfc9429c3ed95f40c962981ccde2be0d31b237b4f2e9cea3d6edbf3ca3d95f2697a186074c44e86b14684a83685e232eb9f0fdb4b57a2281a64cec53c3e40c4f8e3229ff601eb8adbffa1e0af2f788eb7e20c650017d5622275997289e5028248bf3648926727dd38377d3183fd9058bfca6aaa000d9b31b1a69ee04b8702de4c5ebbda8a335ba86748ffd04cb0cd303230ba3133316fb38f7dfa58a29643eb1aa3c50d578ccbea9e1e0fda57190d1f482994d923070598536467d7eadf3924e79574a00c87ca28571c0fdf00b52cbea4529e4ad2bbab9874d140131d6c8a6cebf04df9e866ad58ccbcdb7cd7e0d80a3b52e978bf56733c4bc3fd9441c431d868a4edf0faada6b79218f693f403ea0292ebe97bba10d6d011db2f47997df60d3b17e7f3b5a1b00a0f8fa89ba3bf17c673077efa9be0b6b3c4af84e49f00e0aaadda205aa6e05c25ae9bfce1934b1a6ce6589590d606213cd8874d08cf1e13f0903092442826b325b1e22633aebe7abb2c5a4105df6d4d58a83d59e4dd0a0575d3374f65cbe0d90d580eaa2c0f7526ee58eff5b14772717e91fbd1313331be32343253503e25ef7bd351c5dd392953e46161ff5d0d0f62af10e912366b0acc0ce2b03694a674e99fd73e5c34a049ceb2724af70df0df12f404dcd62ca043a43621eb28985feca2b58f24f12748beadb725c3b919f1e499b8ec928b1886fc3144fd8416b7e1384ed1c8f285318ded7bc20057e7af5015e077f540ae701a109fa39ba7617a4ba755c094c954e659173ceb2385d6e3bb4d9472192404f487709c8999638e80f1afda72b93c45c53ba6f5068a01ccd90fef3606c424243bb6387b2ce10b4adb8e7118e9522e43daf1c32fbb3919b0a6ad8b931d81ab7bef2251c4a07367f2302cc18ef022dc7c6d14f2044384c2fe44481533e6062f48a3140648403ff0e5deebfc42d5323432c2333533b1133811cd19286c7a578fb5a2ebc173fbc9aa72586f2707c4ff668c703a7519de78900f25c5b50a0f6dcebd4162f721910fbfd15bbd612a1404711cb5b2cb17b54be6082fa66ee93a88f19332233811378a5e510b1c6fdc0a9e9a5c2da5542c67bc55d60e445d18e9ee1b3389dbabe78b51bc76e9ab7868230cc2763f83066562c0ff1b3595a389437d909620b6562a781af16b38f147d90abe2aa8daf2b1c7a640ce6827f593cc5b99d1cfc92e9a6cada49e3424f25857b09a97f73ba802b6573fa5befa54e30c134da52bfa66f067874cec32cb5cc2fe2c9838b017fbfe8d0291ba13747afc26b110de382dc6dfb8aa84145867b6dac98626d527d5b7e1227cd22192d9333533c6343634290c39b15375ed3ca42bcf9190a81c2c48dc75e82bb5b1a4322f1531f023c3c212a1268d6084dc88feabce6ab137ebfee05670b2177b42ce30935235c31e070d811ec45fe3819effccd95f992347298112a3ae1f074769ed658a955dbee5993dd18ca78c86cc4a1af821e6c182a4fc5baea7112d60e41143cda30dd7060a053eda8743d11063dd1b4ba204f1a09cc5b69a5ea22c918958828121d716340fb515a74c768d3be754d222ca6857921d97b6c2a964987f6b820eaf1626f5cf38b71b8d793b5f34754474aa097855e99a18df8b23d576a0165c7811fe028a161aeb9c2e19e19677057111f8a9fa39ca3c5c4cb16c68a9a7d814fc298b7b90cd5845401015349cdd343634ca3537355e4fbb871f2b8558d575ceed22734e180bf0d4a4c7c621cefae499c8f24639d3d1aa8f13d47c6c9631b75a12f1e72d79b042e13deb0c916973d83f9076d142b8832f7e4f41ae71916208c5c6b24af052f48afa04138af8984a28af0607838e23e1e31848b22eec4c7e72e144fd02558bbe5759bbc3f1e847f0d193e46e666d8daa17835ca0e6ca11e495eae7c8c461889ac428699bd497b751bbb74727a54d85e36032d8dc3eaa9a5dc925269d1a4ae01e4619ec15eac8d4c1105882267e6139a74ad7de497f947d5d65f84de062aea9cd3acb210d92b5d03b13d41d6f89b63c2f17554c9c4a14f52d6448315fb50393152a35b4df45a1f75c3c451aa29c7e78e5d2b2e1353735ce363836c294bfbc5e104457a4dbf248e4ced0eea040e7132662f2de633f80dfbaaeff1a3139c9cfd936b41af0d1643fdfc10d1e342c7a7536ad969a02bf715d3cfeb3b88daf24440c7dcf8a1bd177c26ca5b93af8137f5c9c66b84c2c01ae9e9b3e735cb55723cb0ab8973f2907bd89ea094d37b0b5b037b8a9e218416514dd53e992f560038fd45e52bc90c741afa91dd3067e4a808c5b321b93e520f634496572beb163ae03933bb9f4cefbb0bf35e0ac9db88dd7e048325e01332819a3032c94030299fe3bc91839050e5bba59f2cf90fbd53aa061059b4a3b9a1f30a48e559526c0e143ddbd7c132e92dbef232b79a4aa5b33667b4920cb3c23d1e28d74668f075b8027c9e243e5363836d2373937fa9b413f5ef03909006fc963360d9b7ea2e825d7f6f0231bafae7a1ed3d62cd4339da15f2a84511a21cbaee19493f7dcb1dd416265207bc6304e9ce1e9a20d1f9d935c7b14857ad879d672a66335963a7bd3dcabd5702f0ea05ccb922dc874b82f3192bee40102bbfe7f6ad881aadef360b6c6e29da57f6a8fa754b51d6f2042f601f8a883918336fa085923c923bed738ea62d5803ce38b1acaad9de9955847728dd852e41611a972ebf768c7d7ae84c5f6d0be33fd7f1510b25c6a7d2684326950edf97fc57f3c39b459877a08a2455e81be067a84360f8b69a893ffa452512cb61e2ef620417576e2909be2f631bbabc2c4591ec65609b2bcf65b5acfe5d7805d4260bee9373937d6383a381e9b32fd3efb99ca3f5a5bba718845c8e4b31a03d8eeb011f73870925dbff1bceca87617a24bec4a36089e9fcb7bef74f28a962230bac4ef126ccab45f7cdb81b8a14ea263257c584a138d71f49a93a968519311889cf83e98d214e7a364e2ecd83a3c60bbb75b9e6c62b3c77cb3f65ea1d692d9263797bd42835259175209b579aa6677bad0db77fef55e7a9e3f25d132a0d4b35ca63f43fa3dad5b78409abf8b914127ab034d73c8b4a5bc62aee4c41409d83d6ac3fb6f9fcfd845c3473ee41584b7558392f05609cd4a8b4dfab8a0a7acba289c700a8085fea21ed8d8a4b6e55cf5df3a497e3be2d51c126d2ac455c6636b7c558a71b719436aff9f84267ab8fc3467ed383a38da393b39239a8f01bf350aa86abda06bdf2738bcda0f4253856df15e90611642842a648bcdced439d56336c6857a84b3e1c1e160fffc775e2a87a1061c2a256f6b0fba6ba338240e79cc0e016dc630c3a411010ba9ae7e8e83f84d3af0c47003954357288d8c0a507a0a6fb665c05df07941d868a1f62a78322788cde750b3f1ec28a99561fddb84bd6a2429298c2f599c64fd1d328b34bf90df6d8c9fe822ed4a5fff33b403b44b7dae94b064c83c9ce9e0aed876a73aac0936b2a09cdcb730bd962aca5502cbe6b48b02f7fcd1020c931392aa92656b4ccb7e79b5411111248c3b30754064db4f7a3f391296ed2e9e28109773a978a3710f2b58e645496cefd30da3216d03c6abf1393b39de3a3c3adf88fbb7ad591eb58fedec767fd4eb6bff03cdf60321ebaff44317799aa85ee638d13ca68b4c424c6a815ffa0cd1eedb54357c8717040e78890fb7efc1567387c0a236c2c4250fd4da447f60a581d4f6ca2da1939ab26bec1c9dd51bf4dee8fe0da48a6ee9cf50b16e086bb4e8b32c8924ac8bcc5754b426094edaf9cbdf34fe247d2dc5ced08df6a1d58433c45a6fe73e42a33c8deed962ffbb5d7b8c8df91958a61933b802583c5da6bc9e10ef9c4bb5d8897c60bfc2b773afc428974cd4f3b64b3c6a67cea6455f15d0578d76db3a4a82e86703d2fa895e86feb1007eab7a66a3273751ba7548c0ecce9a1f8ee51cb27a1d350b3d46fa85444f6d1a1e516094a19fef36f53a3c3ae23b3d3b1e3f620f3d629f580b20be3cf389a6b67e97cfe1d1162cdd4afb826c2069bc397ceebacf2272e01fb67ffdbe3ae122573c9b97e42fb10b989240b057909797c7991dde1f0244894da75a8212532da33705ebfb44502f912afb65c886ef2c1f7dc5404a84a6c816a6122671c4bcac3186aab3fbf9bfbcaba9a5f811b6575e0995ca5868b2bfb7d733fdbff7dc0456b82a058563aa173c9c6d54fa8cba58ab21e7a87137bcbf637d209cfc5cf2c256b6ce514cb5f14dcd71c7ffb57a51119716017e28a8d274f2d9bbc03a63b66d3c85a4862b228967c896bd081bafd54e5cb061722f96f6d503d793de1c1e46b5ce1e0c67c3da214ef680df6ea5986eaa9a3110e820d10cf93b3d3be63c3e3c4eacb9f7be4497bbfc15f1faffab085ee3d9d2d9530e31b497f321cc9fd65f7b321e5f5e99326fce871a6dd2ecf696d5d01e1fa592901c9f6f47b3079ae24e066a9669cab2305f6cd5d35fd3ee69a891866c880efdf480834503795156963e6c727a47e5dc101184676e384e90c414633bae05ed13a42168b0ccbe26159ed4faff8c28081f74cdd7f5379907fd441f7b55f37ddef67531dfaeb9bb6e4cb5806180ab8bba5dca311c9dc59e1f523362a567e625bda442888fe0fc26d1fb22d89ab13f6463483039b5d9950c90fd9e64adacb77309073eb1ffbd47e58c3512f191ea3257834e9ca8ef4a9b8e73a5b3113faee8c8e4d7872e98fae176509f691f70f2afe38a69fd3c3e3cea3d3f3d1b82ab78cfc35c86fc70b33c02eae7eec4d5c98e6d0733451146f8b08e735b5078b9581a41d1693619c60d45bdb417dfa0c349c49ea6d8d685b4e210d0cb4e10b15bb6ecce3787d18f991c651d180c9a858c40686e104252c01c01ee3ec1868dbcf1e2fe179a4d38b387e963198a09008418c651a55ae33b0a1b14ca94b5d16cb94556d770781607ab0b21f9bf39f67f57c41ca606ae879790dbbefa3f2884dbb526e29e97666115cd8cb1701c60518f5f85447f225c7dddcb97b700b7ffa23191325d861e01b644bd019e1c15d1a622b7fc7e3ef48fd95d47832fff86b4df136e62b587b69166b01330fcb47baaf326247b34b7eca331fec3fd40be16876e2214f53bc281853d3f3dee3e403e59dec96c87a79de739bf73d3c285e37ed555977a923682afc2ac0f213965c468c9f74be29088e0835f281f23d6ae17ecda29c11ed10ecff3cf16c53d8987b4ab1e167f9b2f27fc5c0f4d06782c9424a52558d236d1e1e6bb6ebeb4d1795d73af8f5ebfc33103fa7b62b819a3403ec488c7be03e82ff3db923e1f93a57b66d4d842d24234cfe3f21be2e1445ea7f66d28c10b24059453588c13391e27847c8c8f70a6cf62ab928e808fc711a37e41f28a860ee074c78f4511d5123f71d379fe972e7d282e754466756e431759b78f7c83bae432a30ba8c112508a520d637531895afd9ea965d6b15f25b87fc6d4540d9e0a8247912271a8b16a0be4bf4bc9f984b4d106853e403ef23f413f67de967d7092de0d9397f03870ab52fba809fe832475ec922c1bf8c3f9e61baec3732a744ed5b3c8c3a17ee8c64a8412e3b168bbbe8108ecab1af4f0ec7c58f2b0eabe5622905588556b610dbdba2638ae7c43def1e6b6725010e44424718556b23f4a322bbfe6b2531c53f5ce89cf6c99f2f12aa404d8f92c18774392d60bab721c4b213193564ab8bb2d9fbf21f6311722770b0ae17ed08b2b2dd0d3138a79a68768ac8efd433aab997115fcee983194d59a801dabc9343fd0e593097a8aac61d4f993ec0ea74d957c6261696913c1b7371250c123020daa9f10a33284818c7d421410ff64784f897d4983224d7a2b551d96cc8a7cbb0e7395377eac939ddf9c59c1db89853f413ff6aa404240c0bd65304589d2af6c8d1b6f275dc2ece8e8aaf293e8d628c38684b40c5f024139d3aa06f9fcdf50fecee46d3d60ccf3cad74f2efe65762ece4cf7eb77aa94629487b099df7e71233f0217035133e6e5776e74300247ebaa82a336509d8c2c2b235381f6a43641242a7706635170dddf10a39449fcd7e37994e397e210cd221ee6431f761cb619902ee2e05f7d612566d0977a477be3cd7249a7ee3420d1fc39860fd7bde4b276a09940b54eabd57ea6fe03ce46dfbcfbc9bb692c3de388c85e528303cb4eeb8c21888d0a2b722818b68737c0653a60df6840b76398d8df11b06f359f6d2b4e9aab48c939fc44a20fee7dfedd2be0a607f9fe06d9384375b13caf1e11a4f5a88d85a9fa414341fde40bfc0287ca6bd9298ed200fb6a8706f8bb96f4fd1a9a6e9b242cb09857653858401fa789e4f4437fae7da33ce52ec62c2895bfb19493f597c01c5b1cd682efa4757c960fe2de2eac64da3f1140cd6422792071a90ae57a6bb6fe5d902b30a48198705f61f76e7f6df9a40fc746ce72a39dd7dcf667b1b95b19cff40289c7099dfc76ee7b088bbc53a1f1ff88980ce3d74bb6a3c5d8c558745c4878f5f6ddf98cf0e299c015591205548860f2affc9f56899d9e8bacc4391bdc9444d50284138e80e88a90d0bd1ccc049d404e85939e893e9d2d05b43c12f8560557af2028db61f3fecfa89d169c583fec249766c99632de5fe43872412f7ad7268f9cb107043840511a9185414341fe424442896cc5bbba20ddb213efb143080643300376fd9b0d7543cc52abec4f2eec30f0198234a2737b1fc62f8d348c0a14495680a36e358d404014edb45c5a32bfa150941a5ea789cdf75a0d4fb39621cf48c5f566bd8275eadd66f83a95f5057c03adcb7bff14a1a9d14cb85c09f4a30f1637a7f7b5bca5bbbe378ccff67769ff420276c2808908ffc477ec96c69d50a09ed2e3f1a3ef621f0469ca815a01b7b7cbf897c7855685bf904b38e81f351a2a502dd30862d9e9e962b055692accab2b167df7b1ff2ca96e3439cbcc8ec61c8964948f8fb726689367329c6f64489221ad7f75688f7a777d8e44d2236bead480ce414acf10ccec6357e6306ea0bafc86a2b031691c95854244428276434543f6d368a2ebd7ca9896f6b107a2aeb4dcf89f70c3b760fe9a218244bdfcd69fe86212c8aefb70201388ebfafe8c75c7a7d4ee0d47fa818a079fa62003e3f41e5691533326ced4a85f9154337cf42f8e99887d34fe267ecd0a2563e4375259deea2d4abba5861377bf5e09f8d03a2086eb803b958cf331d78a0c28b8aa0180bc497fed77d25c748356ea1d27b596e2527a6fd674f439f8376d40e5a094bd433789a7312eba548e92e15b0092dfb52ef0fccfddf6d070e100a13e8efc01adc32c97b8221de17d2295dc58174f78adc57bd8b6be28eb49c92e5198a00cdadabe735994f9d82754268dc96a110b8026ffbffd67b151df231693f047c40b804d4ba316519932459943454386444644ec83ca2f4ce144417ecde260250d8d115432906d9e5f51e6573456d945b1b56a3297b9cc4164187196507a87df4faae4cba6737a6921a84184fdbc976f9850e78b56c46ba7d4e0c56180081ec09d56caa27711e1fc5e2a65abe46ace2a06a31cf8d29a1c03994820d446e6f5d9fbee748b4531d0c2059f663574b7a80823f8b14c307ff74e730ed799f3d9ed43f06a001c381a33d79c2966eb5cfb268cde7d7b85ac30e7386b49720f222e5d89e66609b4ef3d51837583e18ed683f3f81db91d00349cd4a1be650e6e1ce568e7e962b5977763bf2be9f9344302fe4e9df775ae3bb25fecf514c16673fbe06d65139a3a10a0609bbb53571a06d725562ac780f76b6084dc9d4446448aac454745f8534e2da308ae8bd84aa19ef9940d37a39708e05b0bd92edc822b387da50dfb39af3fa7a6984733a20b95c1a44aeeb256658c0460a0f66abf91766aa6cef8dfa2a5e7498580f91b7a3e4cb26382c171d003c0e98d69be3d598cd42829afa84e2dac08f1442ea92275ff4a9f58e4e8a7a7b35a76be74d29df47add965089880f2ac8b3748100f51934f5b969b978c1db40bc7c7cc48290d301c3d752a959771032b4bbfcb0d5148b7bcd26471d0698c9514de7de1c48f6e2bd115a124bf5ce026fd884ebd2300cf71b9718163b65a2eab0af681245c3964121a35e8c37ef7be04cc7f6b01ac49aafa006444454ad57184b00ebb1a16ed3e94d4353b1a9ae04260334d4f197a1854547458e46484686d11cffe54a1f86640eb0182a4e9f38706182081030087d246b52548a4611463621ca321639e69aceec2e5a439c858999408560781526da2c38680db8cda4f372900608770d26c70c390a9150b5d5302a8f693ff5f8cc75636ec270a9f9609674a29a896a495ddae3ac04cae021262353a333f041c3deba832395eb18c5cf82f1072c6ffbb26870b8a5fe294b53c153a0361f5728cfb5b43d39e17c7bde1826f08d4ad712afd88bea8e48cc160ce6a80deceb9626da6b7c47423eabf806ce945035bcfdcca1743ba1d93a9bf27fd15da7b906b780190b64dfb2b6785e84741603edcf6e2cf56c3cc09c67d2ce41e2d4b6c66cae06f06a52dbe46f7335ca0e19b7af1e11a4bed0b001a585aa9247494720fc5fe1d7b10751bcf59fe595ab08cbb59249203c40344bd825c0bdf8b2d77c738c74c40d9c4598aa666dfeca49bc31870c2ad148626661a2b446a61e9462b4af8fc2b8be5b0a10c977b896d22905e8e4ce0c65d50c9a821311663684b19db7e0f2eef8a979dd1814e03a92c6c6e7eaa49623f6b97b640fa267957cbd7159b03839c4219c1e7a12811f47f2f3c328377e64911a034918d4ff77aaec189a9a7798d0a862c45fdad759cf64fabcb129f1752870f6f75fcbca35141b06d8fc0878bf01ee0a73e6f873167cd01a33ccccb9afe2f8477cf3a74b7e7eede872fc4dd4df44a6d8c61784afefbcc0b173516fc8a15066d86d3e3a0a4f3867ca5a5235f57cbb79d3a947494796484a48694c5cafcc17e32a317db2e257fdf3dd4a7e80c34aff0242818c4459c361befc702d79cf0ea22e5bd5c5c85414ba88d653b57d776dd0593fb63671d6021187fa850de36095e676c34ac65f1a2e11d47df0a7d62b40ed0974a416f11f4c10b5367db23179035d507da69fb2e48152527a97a9ee6bff72f6e7a11fade4d61f9ad3d45ee19c114e94ff9cf19d3152635754d74444e9cca2db4b211ceef8efe0ae958d5bc0b216ac4bcedabb9f73fb475feb1ffa442f448297d6e2c2afe544a7f714da6ffdad8cbfb1f04e0c36d30aac56e89384070ddb155cf1ecc956e33fef33a4847c0e2ac5fdc57350cd7dd8fa4f1a517717aa35cf9de4ae16d22081bdeb166aa5e63469ad484a489a494b49385c07af4904d1122809fbf312c02f117eba738a547a77630fa58200dacc5a9f908f17eccc84f31e4ddd6e139c516515bea0a99357d972f115277383d9760bd7a9e9ca61f9fa20a9b558cf2a0829618cf3131cbc387d8764916c936272ab84066579cd9d3bedef410917ae58686096d9b801a289290b11c75363da3f4418804a7e13b29d9e6865c9d6ddf6f2c62702382d16adbc6021c2697bfe3998060b278ab653937d0b227ef45ecb51139c47c9666eb14ce4ffaa396a67ce21163336a74143ed1257bebae8a39dc9f6d24038ae5eaaa6c03766bca5e85122b313fcdf7ad03103ce2039143de62bc7275272c2b9e0ba748a698bba509022ebcf208fdc98bad7d536ffb1494b499e4a4c4a15684f9c66d0b7fb4e2aaf123cc7d5b4010cc2c4e08971f76fd5d536d1eec9be48b36833dc372d73b72b71804f055f70b291127fbf9e804b6cb5444912fd8aea8fc441125c145ba419292ef048f4a51925d8338941d6c8d8cd1da851267c6a5792a499f558cc2e8b2580851fb654bb18962b685bf1aa0cd2298a19332b356e3f76eaee036987aadc809f6218f63bcaa5b1303db19624e8679fe962efe24c65b5a75c7a01bd239df8df3a932ac373c837fb44c37f32608a560c7bc6e860bbc885aa2c2e6901e4f26fe22199008b139e90ac726e1715d127b0b1e853d22b155bf82e8e0a0aa394fad1425e38d5f60e3e3098b4c46c02057b7a5ca9b3eeb691b8be6f10bf56b54a4c4aa24b4d4b0f28e2b44d6b4cfbd487db26bb79d79ebe08b68570cac54ab1003fdbd7d0ef13c4cd7af1d49f35762d52765af85ae92e97a1c1562565150077ed2db9cc56175a897133bfab7488d3f4bfbd13c5f05ce884c8d009ca2f2db0be284694af25d0eeaf5d247e9134a9ad93ba9bfe90ac75168ea74cebb9e2acf74664a5a7bad854269279e6015ba12be2f90071949483163347bfdddf948d6f4b0db4ae73cd6c22ceaa7192913c68eb4cb75aaebc481089497f009397f4055b9142840ab0de173ecd287d6042928bf1d91f9040e20b4ce254a16f7b244a923c87800816f3e4cf7341fd93c2b04559ca0f6a56e2912e44d22057f7c8bc752c86f0021cf8677b37f67671adba43b94b4d4ba64c4e4cbcff0b223bae1307dddd89cd14c1d15da0c41a09112f8da701dbe23992133c70a5f0af320cd299d5d67abfbce7e8646962e318e35d09520066249520b683f571b60332fb1e6cbcb201614e3243fed9288d32bc0109e532d5318991db18fe3db9f10c87d06177e3f810dbacbe7a5200428acc25c2389dcefb5c87c1b9461f977c91593d17c627ac5f569e9ae06b9a0b11c9d092a79e7b9e23348c4b1d7e9cb96cb4b2a4647b46388601b96252fe4b532b2d25f347e8a47460917d368e7e4277991b56f78d8b276bfd5be171236bfc36848e04aa84536b2d7902498a61b862834935e067ef85176aee182a433cb6528d328910afd22f4973263c49ad5d18919785e1575eaebd4c4e4caa4d4f4d76883e90607a12c9edc90dd3512cb0e593bae4f465ace754d77c6e81761606d0c3dc3227723ed677b4b497909556e4db6c964b3dae9826051e7d37652f536b21afc384e59eaebcc810af3bc50ce595d03a07cd5d6222118766409b18904d748ac56cc4c9cf1d84d15ccfb93e56d13b46b52f1fc416c9a09c328704322ef8771d9896ba9b2643fe327182cfd3da7dabe3ec92eb1031b61f2bbee488ecccb3b566b67d5aa6a234709f06134194d3244d6cb1f1bebcfa0b6b88da7b95c34f6dab787df0c0f8c242d6bb3fa4f241df7903e782cc166303183d6f76d31ac2a7b46b53305c6089f25f3482951bf82fe0f59cb0f0f4b438cbc8b44b385dcdc9fb392012894ef77bc14d4f4dae4e504e5aabc49947172e091fed37123ae7c30a9a44413b4f2eed2db8d576d709bd9ca4e501aa5f972299d715b22183bb5f60e53528ac40da2d7a61b7f9f95dbecceb42abbb34245a1fa841d03f59376c686efbc22d484522c2c112f10a87a8f13491f2b64633e621ce85dd89eece950b17c9a891d8e8f6c22c0cc4dabd85b9b1c103f99040b15416e3656711b1163ada14032d469225045ccf518c7006442ad2f09a51856721fca0a66a53d146e12c009d7e568cf4616dc57bd895fe64e0be7fe2f54cbd146e0639b4f2550593bf79eebe7a7caa1171ef5296079d338cbcfaf745253fea54b5870dcb27c74a5d8d2b34da1d214a00e7572ca90a429f2c5bdc71d1409588f1e2dfc54e504eb24f514fdea88fa445476cf77c9edeb1dcfaae6e3cee28015924a9a6877fb32d8fa7f23013141ea92e06e2ccec4efae0fe4df396bff1a51355e4693b41d562b9684c6d978bebb7bedf5c6bc014413a06f383042c06323b1892af5494527482396fd8d8e8ea372266945b7b1fa383593d5cf8cf1bb3ffd6c611954cac212417f5ca7851d6507e86387df054d184d7a56610c792d03ff2c16ac9f8101de55097d00b310ad08cb467320bbeb2d3257961d1360f631e13004e81f893104e49074bc5e2d707f4a6c2b8ce7b7da5270b2c2ebd58e1d9e5b4399bea09a06a2c22c2fcf6de08f994547f34beaff36985c45d6ef11583818909b9833c94ec9bb7ef4b0a43c335352f1eb83ebdc94f514fb6505250c48945a327d9973435d58ef0dba7ceed8c0eb5e1c8165f3aa54697fa30a6464eefaf7c51fd4c8aa79a2888725de18253f0f6ec46a14d8731ea9a0ec03de64654947bced71d393716146e4e1b8eca36dfb569973d2aec89217666392de2df4483223e3bbe6bf86122c105e0c3040cb55b816fc43af23ca85bdfb235696d827c3c93f39e9f1e58b34a5d07eac8978b07f979e7e096650755fccc3de2a77de0f1bc39456d587259cd919bb7ba969b08eb83ed9cc46988aecdaba5d0338eaff4bad5291589a3261ec7324b319132694e83b8ae961d53eaaa7ed85a27f69c325d5a1ea4ee182b8f60ab7d0697200fb532d8d94ec54ef3af9c912850a49fe6398321725d4a60cd505250ba515351f391022f17bcff5c049f811929d1cae7b13d6aa2be41f0ae0eec568395820cbe06f89b023369550c3d58f4d96516e99934e5b27b975bef79f1e5f25995534b2a8346c796055e2088e9de2263d86c2a860a1be6a29ac46a19a343a018ef47b4daa98677b9e2dcc3c66eaac0d722ef497ca9e00d4f2f6a64a5c212ab8245e7bcb789d83bd9d5a1666e0667e2ec3d86aff84fd4154660a23a095bc1aa1ec1ef28508c9d76a35b63065d474f286ee1c4402628daba4b66567ea063be9a6b52e988384fbc23870789c62e6574b755608c7150945ff7388e46963a08564aa433f3b6e9bc97afcd1bf5094181b22063bc4a3a4b70e2ac04f2364dd9fb81831ac4c911f27d54dbf1d1515351be5254528b8c9cbff43282b4c4835e0c364607264b373f2a4a10c8270cc2b76a7007c14336f3539fb1207ace0291f819b7e05aa2471fb5fda9f14586de92bf3b878c99d6a67554125d940529ed8c953b0137c5ed7d193a9dfe94db39bd727ee44caadcbdebf21ba2aa6b945ef162e23dae1fdcda81cc26f1d167f489fa0a428b58403b83a1c15d067eae1aa1481da5186f8443c4a634271273a98b181acba332b3ae4c3c970c0e7857e91e441565c3c97221db0804d6eaa652f51a2a8f0d64756528f1e7623ffdc353475c8361aa43022ad113b8a08a41b4b41076410fad875e4f413b6954d4e100a543a4459af9d24c21271cfd140498970799081705f182b4e2035b4b92af1e54d5525452c25355538688872cd0f560bde72c575351857839da73283d421f467ad4b945a397e5be9cfe1626654f98700289dffa967e207efef11c55897abe107d1311c1afd9ef829bac1b989f0622778b0d63c2e0d88d2bba3ca0bd9f8c4cff00faee4e928e62eb1b3d6cd6b53230c8555066cc87f2997388a0f484ce8a18e826eaff51e297a10b92764e4120397737334e819c063ac4daaefe4aaea4d7d348f97600ff7ca34e9672912c766b0f9bc7bffe51644ccced62050e1788049301f6826366bd4491b33f2ee414d527d4f2c697417ca9290449e1e5afcec9a65641d0c75adb2f1b6f945c1f2b15b836645f70346b1187bf6fb064c5dd10d922ceb075a6c7f90f895f8511cd45a6d043d9535553840bf81380c4e03100700403056021088498280201068304a313d802284c816054043da418f2b0d549b1f5363318aff5e80cb88105cdfc49f471a5418a1cc10b319c63c44369abb5e05c00ba588fac80cbf8c10fdc4cfeb8de018fa5f702b5ce31982613a55af022dee2d7a353c03594a43d67e223171d468824bd58d339467e98b254fbf502284a393847c79bc4918b00a554b25e6cea1c83b354652a9872c5c103c1d563722be452445eeee3856b9c6310c1662ab5604470e14170628c5c15b812297a81ae730cd8c1a69315bc0225fae85124e018da5523d70ecb42582f7ce81c83293251aa0523820b0f821363e4aac09548d10b749d63c00e369d032b38b92a01f65711b52001c7e02aa722b48f5c66a8917a7ae1a7730c8b4207b283d81f1c8c244431a2479180636857955c3b2c0b61bdf0a1730ca6c844a9168c082e3c084e3147ae073411297ab1ae730cdcd0d68915ac907bf1392847ef56c9a588bcdcc70bd738c72082cd545dbd13e53f8813bb00f40812708ceaaa926b876521ac173ef4420cbaf5d3b6a16490cc815f9d991e381f9039a0093ae1955276179a3bcbe0922b8ad039c6382417788e93a84b2f4208be42d298bf57536f611f40ba588a051ac80b3139c7b86879ce09c03870a1b0b232965cb430735a7ae186730ce940ca403db8ad7e5b3c8345e84ce68f8b925c34315392a8bc5891730ca7dc9404edc710dc7a245c4b7229e28724c58b3d9c63c4c33952a8bab802e73fa85c32e55c3c07cbd18543a1c43ae19572f6169ad8028c2ef58b6802c1947ff5cfd2953ef05b20a26e44700a8e9cd8fb546b9128751557106304c80b3139c7b868b9eb9e84dda2059ed7cf8534988502a8958b041d27b2174e718ec180f11834d98b36f8eddb8a50875ab9a0a067c6f4c20fe71816081d5cb469177e90a5ccc523489180637089fbcdd8d713d883a82aed2e327d969199a864260a62285be07438e32210708d53b645539a72dde187617a17703bc7100f25ccbb62516e101e5cf01004ae9602d7ae76a1e8b38c1d920e1100040357091152019250b4b8e0cb39463e941a5e0fce3a17663d6a0356c067367f229d5c6f40c7e778211ae7184a985935a9e2620e66956f81c2cfc10b6806ad9d7d72d5e14aa4e805bace3160079b4e56b000e05e2001c7e8aaefdfd60daf28b3bbb0fc5906644a916e10e61a34381fdc7104aa4aae30341d40642ef19c6384822d7d8c2010fe5c3c018bd1b799472e2d9af932d2827393174b758ee1c1f61c1502a1b7c06fe096302355729520eab4f6c20fe718d28194817a705bc7055b8f6cc015f8d4ee2fa493eb0df8381d2f50e31c834836530df55294ffe04470c551cac160e608d3cbd85d58f62c0312adc83388458306c7f4e7e213500cbd6ccf292eb9d22146a4d2a26a3ac7c80f52966a416d01ffca7a64055cc6afbe1547f291eb0e71587a2f56eb1cc329b536a1a84628e620ae10ba4090bc80cd605acfc5cb492ed501857872176a3fcb888f24e6bc0eae800d3e0f6e1f81b817f11cb9d8444881488b8ee51c231e96d87115e67283c1b71ecd02af51aae66b88935ce81022bef4224ce718f243b62886ba200efe8331c07839f81c8023a03b3ae1949a76179a3fcbc0ac2842e7306008452550d8f51601c12d4ec9149d7097422b909125b108c40b323bc778b01d2ea4604470e3b13a30945c1418315a7ae1a6730cf9205b14b5e085e0ea41b9557229222ff7f1c235ce3188605ba23adbdac20f46402ed2e6650a788d9235df59bc972867180d1cbd407f1762e8a2072e6048f7d733f50c903bcfc55ab01d45ad92eb103326042fdccc8518bae8996d93417076e4d2ef11b0601d5f3e544c8492eb0e7159bf17ae758ec1149928d5828bf002ef25a67cf7c6c4da33fb975c3a3c11377ab1ae730cdcd0d689156c815c007a040938467555c9b5c3b210d60b1f3ac7608aac95e8d4823aa700102ae6c215708d2ddb8b98965c77f861684ef0cc35b2730c9a943edc30c0bfad4766c00d1438f3bfa2928b0633a6052fdc708e210ee688a216dc16df0259ebb60296e197cf3723d7b8c8504a25ebc526b410a3bb3a7ee19d01697f26393cc36f9a5b742dd80e45ce10326cb90bb59e65c4c3529073759dc353a035b88870608d5c29b01868b6a8d29c6328d82cd49101a3c18584ad6e516b1ab968305258e9c55ace31e2e129c8d501dbf24485a3a725b88a8f592b267487d1cacc2e207d968119516443c1d2e02261dd29fb6925d003e240b627ca23e7184a38435725880f2e7808ae30325c1438123f7a117e2fc4d0553f6f6b24836cb2b6ec3bd214d50baee3cbe3992554b89041547deb45a4ce313017954c5cf0fee09c5a8fa680d70894b743964268c2e529cf1ef883655fba7b71f2c7e0675302e423020cdf03c65456547ab10d073158e059ec7c0d9721e4602936e07110e6ede8b2643bf7faebf1812439027c60e405991e5ef4a5b81fdece9a6e4edcdbd50fc357532be62fb29cfd16d9af59784917cb6796c682240e1fca46c83e1cb15bf88d674e04c41d00fd9e0c262adee0d03981aa23bb9eb2c158f124368133fe855c9df23bedf9ca53ea92737f775af110711a6fa2df5fb85a39dce229d9a83478628a26f66bc632b55a2423b5a1b221ef04dce4306b6a9c756d14feac47b9108bcbca4da60ae0e66e976a1dcc0bf9270f2199d96d6054c043e2a8165496c35cb80c9acaf931a59e8b0b9f1e4c352fa7ffcd94fce27831471c9cbabf00586ee013f5bcbbef2371bed90c3775e0258fea1edd545654ca5557553d61c4737e897968adb92a43fd53149e100b3787fa564fed451328602464caf2c24b71d4f7f58379998da5738d017b78bda8038738c917997d44b19c63f300a6b8af6744f217b0c39ce25ed15e6891194b8106db0e2a7b550358e5decfdafb6c5338b6b5fd942ddc3bb05ecb3d10094999a9079ea8c8009d9b29be3eb8b7bb90c4e9457ae71c84fd5c7c7be71a7bd2d60a8e95b0f0907549410412472b9834478467adc87b136091be91286462665ae8aa0f0b6996139ad86f9b5360b6c2c28307ff4ad85ca9d68efe8a328387d2703e946a2404a7f4bebf1bae7d5b6ad878e6ce8bd393640c9ee0438d56a3c09bf977515ef81207e55b448ef4c131c143a501f52e37e2e1555755ce565856f601d6fbe66acd03f666cd782111391da448d8d8e5824b5d0451632aa95a03de3230258537b8c6558f296d29ddd557a7cef7ed994d242dd7fffeb96289da602f0fb2b4767068153485d5b13b8bebc62aef233f8fe3ebdf5b3d732daaab05a2b4081c52ea3ab8aa366ba9caa1111c06519a1ea1ce4a385aac5e99f64ad1079ed53780bf1756f9bd70fffbb5c3ac40977180f46461ecc550889132ed70e7849cd53c9683eba1aa1bf371b7779549bc989c78387c308456e4f2be7fb838e8fbc6673e73b38e5b0df06840d64d08b5f6cbc0505ead38a9dcbe4e2e822b8e52c1102ab5f3815884e8f1148f96666eb804024e522f2c2865ad91b32b6859f680bdd1e61cfe5be307ece5565856d25759573f5648836e8e9e213a2c75a72882013d3a28f34f1d429c9f363344443d5fc6f82f7b87697b6129c49356369efbe828140b018e3eec5dcede473240e4bc51f99cad95e709a680096c9a753e641924bc4d22e520f999cf0856c93431ce079f43e3493edde3d392d8d0f326b096fa76ca37af0ef36b95af2e5a51a9df497a10a5b1e7055487d35f1bbb9f6666786ec089e5aa8e80d711895f56b301850eaa9342dd89696c413147a09da3424bcca79b9df1f11053ad03e8b7b22d4b8b0e378e98e9be5e3aa881ef6c2906e2b4c7883c2ad486fcc2f1ffe3fddc1d13b18abedbae6a7bf2e73e04b3f662947b2af73034305655804dbef26f7c713512d9c6a3cf832ca9784874e9575957d6585a58d7ad5ca60d0326f21024fce01b5721a83c9c622c128ee8309d7af783ed5a476c3230d5543660e913bca77f7d760eebe074ca52afdb7284e525f1647bbe4e90e5f3a390021d9ef2731de00c5e438fff2c8cd4593cd033ed4f60103a3cdfe8452a20711620f79eae3487e7d804b1b4e308ec79a8a364db4139e814ffa7cf78d187307d9601fe8ef8e03a437f8be35c712ea3cd75f57f61047a72ed160738403249ff04b978d7e5313227c4915449e807608007d9918070b665c52f8b8bd94e8c303bc62f8fdee957d1b953cdbef00f43cd78e68bdd91a95a73595cbfdc901a6f71e7c298c843e8448142872cbfca9bd66fbbb83f7e37dc5fb71e7ee1ddcea93e8bf52ed8a52193ed85585a58da595b59b00a63cdb31c33865f170d223ead402102c31696105eacf9a3eb34f116528ad0c211b4d75992248d4b50a818f5f2cab9592f5717084ac3d7f233cd797422b4d4808e34fa9a9ee64fa97b1bca7583df52b243d3819639d55cb6c7bfa439843aebf7b1ceff1309910b312e833d91ce90158ac223d736f6a07abea35a79c8fdd1fe33980a6361f8d4c9fc7e46c26530847166765c904a86936f0a7d67497cbebad19243bc51a503e6674ef7aca3de55b39d537f09ab6d503156bebb3b13840cb3cede58f4cb9f22baf7b5a4624147b80d9a97f90f7ba2a485904ae114f7f597712c83efb7e46d33d062be1f6df32ef907f6be909d8b93ece6ac87f24a2eb625ed06570160c2f1595b59de5a5c5af0bf218ff3f7cdebfd5929ffa7bae8c6687210b5bffd9f1ea7da8466061b4fafdb152f7ec517bee6fe8e79b56dd93cedfd29acad463328ba14273a0d8c49853de48db2e025594e4c9a21d6a2e3b36daa0be0714c1b0350d0cefb551a46c8fb3e84cf8c8a4eb84c38bb6838617affac62c517445807ae9944698f283d4aa6297ca5f0916d8b012e449c245a2504910dba4b7e9416ca83915de15ed12c6a6d91cb8e4be14e4dbf8bae61d13abc02a148e90b950fbd36c0b1ed8648e23f70ca251a1322abeb3829c89b424828d8d088a020854fc57b5fcebf2b36d5c24b9e1796014f4c89be5da88155bce3735aee57c00223035ef65488b53736912596d78ccb82902bf55a5c5ae25b5d5bac4f4e427a73857d8d2a9ffcafe4b3928544a4fba1bb52bdef6ea8074f25e2f8523952e150376da41c1d00a3e76706db8a08c380199fef39b259789fa3f96290a2bad3496dd38c9e0d0f0a9a2e1bf47f645436eae293290cea1e5992043126a21f489542041265247c02ab8af405e9f988cb338575360aa6084faf770da1476c8df35c0c8f7267641979e15683fc003e2d1902632af1e06a7056cab4cac442c8a10759c44770f1aff84abd44b14f6b50c60ee46c897362e0a6c8b22f5760feb7da3e9706451ebbaadcde2d1ce39dcd608f3cb0ae6bfdd5ca190323151abc594f9c8000880ca0e784efa20f4f1425abf49640d2f20ccb4bc20a33093beb2aa359ba08a617f95b5d5be65c5e5ce54196380b3a4e25570c2cb0b1dd4a53f104da89fbfdf7f8d5bd90919d35e3b5cb43205aed53b5b0c2a855250b0777d794c8c9c4d02f750aceeb964282bdfe15af27a618591d0cbfa0b27c205be184f7d25fae59d8f5012f43aa620ab2bcf96285093b618aaa2537592972622e3d42b0a09720e087c878480535e192056c335ce0ad047c84cb38a90745b8298c2c28920f6ff28d6be7b914e188e5301b040fb1b9f4e787202a8f02b9026c4c72b14ee7467c7eca498d0ce733309067528aa196eaf5868c63e68af6740c80f4020840c8b1febd7d288531128092a69790d205f95100bf79909b44348e78f1a043c3bd3fe44b85b9c0e0d837b8e3668b13d5df13d8661154fd5c5e5cea5d5f5d940836db66bae2d3ce2169cb3aee14143dffd867c53e2ba48dbd3e4ef8d1c07936005a73279f09ebc4c00c8c10270b4d3cc2f346bb57b560b349011964f6381a90ff0985229bb80bfb066bf78f1ba3aae86f2568b8d1f2e3107d163ded1d10507b22a35e0ff18523f40565463871aabd86b6c909da5b3eb3d7613f5d57b57f1cba257a878fca78ce7c9626e9de6ca0008bf79bf73c089d7857f286d5e1bf058d8c438f34eabc1fc3713495003c9506fc70644f3acd77ccae62fc228269a010ff6a76aaa179e9bcc88948c34bf7b878ff9105b39ce56c5c008c185984bac174fd2a3714c5b9d754bca63084cf97f7ae66b397cff6641edb896414fac72ebacfd6dd802f8381865d5f5dee5e605e9c008841534e85151c41d0378311f22dafab0eddb7a8d45c2c80b0d00b82584980ca3147f748c3ae2350d4a18b84832bcdc7231018ae7b159819edf91c801664b072253732395348b4dfc6dedbc72e0d4a45b8718dd56ca74fc863944eab8598e6ec1b900525c34f24358e4046c5b8efb7b12d72251203fcad2ce9b3993649f2ad2522a7f92b3fd99fe9a59a1f3656e9a0dd81bb2aa5112cb787a4f1eec2e99fb831b04ef17cc85ae5f9615b1d36bb67a2748808434e6b901913e4213887160c3212eafe0b2b16fec8641a0ce94f64a9891430f62c10be5817dd84a0b9fac7fd1825c24aa2775042520772f1094d3818932758c70423ee2690f7b0792e83b2323e94eee085865e605ef25f615f056d87e4e10afca07fb014c25668fac75f894a54b76bd90211f4cdaf3eff3263996d874d92356f9c1f2acf66da64dcf513ded5a7baa987c71a37b9c90cf416fc8cedf7f747644b9a74175b526d90d0c79746c25b857a3d8edb69de78beb712ec8f0902ad145e99c01d3d39acf5519cad951ea7aa6a7ee3cd05cd16577cbe51a26db5a573e5e6cad8918bb24b27050a0b882d525929407e2d6bc4e63fae743aaa99201a8fcceb0ef847cf279db22f1cb8e74fc20ce7be9dad781c367d08bdd7256b37c2f1f181b1fd559f16cbe81be0189596ee45e38d218caccde0e8fc1853eb903366e4f175ec9ee7db7d0313255e8fb5448ae8c9b0681b347cb763d60c0c1c277db48e89865f615ff660946260e8fa082228c3ed51f2b62b0b459b50ac956752b6b1a938411d931d156e7341aab5b5a295609e502c3496a97f32c906327212def6ea172dd9c39f47a919d2d41782d6860dc1ba953aeadb07a729616c85e8fba2b57a978e81213298b2b668c1a2f4e04b38b62f9f4a6d7f70c6e56b8a7fb5094fa4cb786f0aa912e390c3b7da624a6af563a396f046968cf2e8c06735c21477efd7a2f12b7659415af78034683508cbf3e608369a86d63e7c345d8999fc6fd04a6590285d15177705663806fd3984198900dadfe5eb7a146f8ce9a297837e27a816facea46bea2acce1f20068308932d1806e0b412b584bc2e4f7ddb5fadb85fd70bb19be52823a86b469912e483138f28d86aafa61a46361a8c5528510f56ec8454dcdc6ce5a9fa0c25ab91cf288f7177f24c8571835c7c7b0734ceecd1d9acd5d668eea2c6ac2742a0dcc3f8d74017de443b3739ab92c1f868916f64236028b33fd40d8fa9d7bc190c226991ec02293488a88edc623de3f1d33b0772b1eb5913382238960c2cabca69e40ab320f10e0303d1483bb70446b6b5ac5e67ac7be07074f49833bdc5a29f9fe146a9226cf02d504e8f7c1bea0e6a157914f79718e4a5e38a9f17a24a2ddbd894b156c02b388e7770926044aaa6dced7030ea24b87b01f3a2149d6ad7396a22243a00f1ea12fe20a496a7ee304230f36957df08951dd7a092cfd73ef983ee913485bb144ce2fdefb2ae40a59f71fe8558c2e9186616361fe62646295c7d12f31b542eac5af0e6516d75baa8d450541aa2ff38a0e7e5bfaeff1518e7df4435163cbf138e797b56c8238e97f0730b32a6b8406d3e61b139cb68453ecb9c8f849bef044c0f0899d0012247168ca2ffafe84f4e883c8d132301964f4921aca39d1bded48b9b320a5b942e617308b786026d8d3b19ac946ba47a2bced532bbda1a1242533098f80b5502b9b8d279248bdb1be02149eb5e214630bb9620daedb6670a31641444070f2d024a3b7153bd711811133e58a99ee1e055f96281f2c75d76e82f7b40daddf17336c06daa48caae5a03e6d031adef98172b388972f69c0e9068c040e5585da7c00de0dbd68f24b2856d94eb3730ffc0597df86b6af1e11a49ac0f47d956264628277ab6365634979b0f75183af5cfe95a9e7f356f565348abba65fdf024cb69c73bccfc7d9c90b390a0237d28f02600049c3877ad740a275de7cfd7173155131d592b71fe54ca4f4fcf2369dc8b78541448ea2530d8a2276164397aa220881de2f15692f8d99b91eec4d39ff08eee7084b3e2ce5a810b45733ad0910fdf3f351de9a5aaf27ab7a2ce7f4ab9117adcfb5d4b104051282122140990cf988d1878ac3ac641712ca811b3adc8f75bafd36060510e59b0ece65d0ae3396a0b1d6caf1f72afde753ae45173165276e30cee40dc812ccc89588a639150765b4542457f1f9bb5e5ae761f0d2d98564cf6220833fc4415c1d4856f0b263a156e09674d2f984b410ba746ffaa40eac998663656386ac646664ff3444f3d443abdfafc26e374cdb43880e352dbf371d382968bb8f155b2af3d7323078252d485800f344fe82a096111f8415ef3a1cb6711680651d121b4f5f2be085a58ed3cc22f1189c1d809a4d9950356f876a1664ff13a8e5f596383abd11c76c2a77dc3b4649231a8037faad302c506fa18222c76834cb86d922e79ba5f817afefa16489827cc0429456d8cd426413cc4ed6848e9e96f4eff04fee165490a590896e8315e1b77605576a717ad46dc57200d6dc44491a5f90d3835d4dac8449a6c0ebc4a826653667a89beba6f7b7f3baa7f1ae40cd0951d117c31c83a7f021ef945e0c6c2d82303f5929e9c011e18041f6b4438cb398478a1d545cacfc3ced08dfc6f5fa9d866466648a6567655a47734cb998a2980b4b5da3dc75fada07dae6f8b6c1c47304ef8be18e62fdbced0d3b2299f0d2034814ab37e3df4153777dada9add61a3254ed533b3436018164f940320446e1ebc0bd21163414c20b217304863eca191fd8b1d0866a2f047662d6f8ac65060dff5390d39d458a73a6dd559480529088dea4ab1105b750467778e770284f7df5ee93e5ab286d05820d29923380d64eb3af127338c689d69b8d36f509efd1a3b71ecf2377a8eff151e7937f50b8156f701cdbdea492b18caafe1d221e6cfe4965f5ebd29ef091aac6a52dead0e497ae1da55d18213e3b8d6fd2ba21927c9cd9d2f0bd8469c701fad44a4e335223e9b98a69b2825150c702af1e11a40e3030eaa1aa8e66686656d3a482733469648d6a427eabb227937970f30afb253d643114f536999f371442e0bc74e1fdd94aed951dc0d08e64a2edf829a16155fe6bd714f0c07eb3ce8b3d1adaa7dc59be89e17beed97b578f7bbc93c229a1c1cf945635473a2d93bce9ea4ed3715bfb83f9fe69497e66d794a2cfa9f729e592ce17e2d62a2b82a4f3049b42e7cc3dad24a1c3271b38bbf2c48b8087524fb341657580c16609bed88ab4f6b017da5a8e0eb67e49912a5cd4f4e5f24b6d45592b43c62e18ce02184877ba094b11291882203d541ae8e2603d1da35b97678330b77566c56820002690f34fecae57cc57dbbe619d1d305c5efc978aa9b4dc92df1b90e4332ccd7883d77930f00d88a586666866926769672a5f06085fb1610679b64e87e604440263f725bf3b289099c99ae6191fbb761fd6f832753890f08966e399d7a3d24a1420f755878512767c36aecf6c3a2f222ca0e0cd53cbcf89ede5c60981e1a4c13ac04e89193e0f3e5f5d07e8f4cf7c9263fad889491d0e834bde0b58d2e5cea9eeac3e9d119175f91fee9614a7270538929d5ee6141698e128361c7894b8912f20d028331ca9b5ad70db78e9d3abaedd60891101cb55aa53ad106c5aca99d7338a23c9b990aa3ae84b42cc045c9903b1bbcfc98b27bf3773ed7743eef2787c19a48203743ea3fb9bc0598533698b8b054ef62061d8fb06d03be1b2ad105086de363384b0130bc63c53afc7a5183757be2bc26686c2a98667696796686a68828e75ff3a979ada914cbfeb4a173f330d6c63f9c1f8f093ff1592ecba4e4a03d91e7b8c25ea441b67f8adab0d87d535c3e10dfa225fe21b4182416b2f32f958adaf8b6d6b43a8d84dc9a2af60c65cc2e8620c1af5137ec6d1f6b3fcfb215cfba5b95533a3cba3b434fcc9e7b9940b4eb4f950fe175c5dee831247593be77ec2237c8ae0c7d6a028079a138c269763102265ebc5d17b165bd68f426e024c86ab8b04be0722ee5fb7812d0384ff396d3a0926fe8d1de4a6a313492d5f963d6e06a6742b19050b20b4ec4cd982c49ccede94599326869cc5251c47f264480a567c5fc7f09466701cd5cc4ece5eb36ac5f4eb984ca83d302aaa718cef721d25c37cd8dbb774ad86686a689a696b697faa2fa10fdbc9a2e9287248f4e564473a5db42d72c5ee1bb5bc41f684eb9fcb54ca9b81cfd423f35c42763b17b80c56cde3eb38968783c3c5bfaa64a31c3f0e85b62c0ea51a96eceb62de8f752e8710358fc6a0c4e0be4c25a27f6bb3c58fe2bcefe4c901b47ed66aa1ad4d28bd5b59b2760e13a7912bcd92ed78836e5380d01a06e8c62ec661eb09b8fa50a7bd823e0fc1c6144ad726f458bba49f2cdd30ec935136fe8fa3cf20cf102d3f52d6e16a6e143f2bb34ef955dacd757b5a726b43fbe955a7b6b5bd299d72c947192c23c395cdb1d426f4f11371b4b0b08fb0dd668282e1fa415cac224b04d223c271e365b33d902b3cc6a23090f41e192416a42c4ebb15c9b1696b699e6a6c6a957501b22bf1e6b5588cd23cd129df4f2a6765eec704825bc7172b3016b3bcf564542478892a7c614d52c8b485171837d7f88fa4882d294f9c39d9372b805d5cb844571e6840a4f0e98d36dc20671a4ecfb8cf7d50b5d257fae1223c8533bbae8949797e4a234724f524fcc392a21af681c2851f62bda94326fc64e33679758382c9e97edf37728f0242170e584740604d9486e3287778724c649ddbff694a65abb6da807569b964e70d26b49545f1491269a34e807473c7628451bbc4d512de362f829cca1711fdf953278c163764d2b1f8872f0679b60afdc8f73222bd11d6b4bdc15be46ad8496320e1905f534a299c4ee3e64c14d41cef27c0f313ef159cb21daf3eb56a6c6aa26b6d6b8233c00a9fed9618b2aff9d1a13d908f363784e4c8d418a17b659d8566f0ba02d2b3dd7df1745806278e8bfe08e3ff649bd1218d416548db834e47a3290344aeab2672d886ffb3966e870b0e529bcbb27cb6a9fafb1df407e153cacd6ccf2883a07d8ac45fe290e1dc6adc109a8371278b9d4c3149ce05c7dba4c4a52f0e9187cbd97c57b85ce52409ad73ab8ca35f7659106e32eabd6e4c828417464fc8390fb17290b478b5c401f56c571dbbacbf1b08b940fe82ec383a71f0d7bffb86eb0d99767f36da7978ad283da0cca973d7e8893b4f54bf48e4837724c20b4cc7161c51a78f81c8b2569148f78f817c5061be4ee62ebbac06a3e0b1ef4773da024210dbe187d5b96b6d6ba66c6e6c436155bf2561ef15fb5f4848d026c94331019ae42ce77a793961c577676c001968509be708356a2a363928725bd7481f4ec2c0dcbb7c09476ab31525c3bc7d5496bac7b88dfaa7713bef4bdb58ab1f28b9efde1e929ff5d799c75b4ea88be7017d52d7dd065baff2ab2f2f86327180ae95d557c93e0a0a27dc2bc4964f8efedf59df32e11b0784adb89982de8bcfb25624376caa5c37d5339815eeee91f56eda8b86799fa65fbf70b9e5bfc0d091ce88aaff59e441aad75bb95e2e4d1f27bbf53f155197431c90aa5207dcc53147aca2844972044bb4a4565c6781b9a9451f9192ed171fd0396eb28b0e85f715014311792ff69d589747c1863c51df437aa489d59ebf91bd6c6e6caa6d6f6d0cda6fab24d823416fe51027173ae97077c23d4ab5624070d0f3d1fc52eb657f67c2dbdbc434e7444e665cd736de0729968264fadf7a7e15322be904a0518cfc915d0396e7aae229c5e9c5ed1c4ebf23e44f91d5d9c3b33f74775dce761468716f2d95675d3fd2181fb1658018a9deb98b011f68bafc81154bb84b1c4a12e34538d28e7caec50c1e25593f279d519a3ccedb8a11bae4214646573e107241ab8a83b382dcd18ec15929b3d733d30290445c6cc86d7cd689d967d4aee0bcf98e815a53386738e6cda9bcbba8eb977160f3b0dd1601895b371d49028d68cb572c4917a55ea43a1cd6c2c34120cf293ce9205b18190ead4a780d676409c2cfa9cc85e6b09379c16d6f6dae6e706e0086a7599bc2bb474c576a900b3b68b03ec93d5c8f9789f0cf3369bb20bff084333610b91a560bff088a6c254dd55ca1e6e7b8aac9f7a468c242bbb26435ebf49306b3790df2d3919e962f0fcdaa6d7b39889c1217ff09dcf4878c2772ef34f2cb84cf6a63c3c1a25728596b50049d5343c2b8f079bfe72b2a3f475bbd206190ef45dc83418ab836dbd5e3ec9d634425b17b7075b2aea69f7a5035bbb1a4b5731bc6a3927ade2b394f1e8d8320ebad90ef467b7dda9973317fee64da4450694233fc607b4e6305a255fa84aa24d135d08e958c1a10e22b32f5afbdf77a11803a9f8c05c22802f96a48a24d636a096ac7b57c9ed671ad63169fcfa23ac386600e96b1fc971343c5abb26f716f3e4741a978c119d103a9390ddd15b3beb6ee07eedc962dc9a34391ef49cdae62376da2824f3e9ebd5e2b1454f20c6c192e1aecaa2d7cf8eab8fc8ad1e8775bd9efb3e9f6f48fac95ae1b96f8da87d3cc6ecdcc73e4fe84378e8e7e5e9886b3cc5a66aaffbbe2737c3429e8c6650f8bbff5955813cf4d23ca026961ec26b4e9e8b96d2c7a42442fd5ffe1f4fce80a44f35d00d7e44ea7511e8494801b3303d5a9508bad0983e4ab00c3109aad7a7cf3cc9473ed65c79504f58ca129ffb61ee6d277518567c5179965a8519df0088ad83e23961e10a05525175bc97498259370916c016aec281a23a06d0cb76241b3332a6f44f1aed85293e4328c7a898af46b8313858f53a1c96f716fb670727033187fdd4f16ec221af0c94d84f8ddef9186453a04fe57452823641241b1b80dbb7994a7cde21c63cffc6e59b2ede2d65320b5dad826b4575ba5aaa932c205ca345e9df5defcfd9b80593b90be31e7dcc93b18f2ba8113ae62685808db471c3bcef6b1c585486bcc91ea7489ac61e9b131d678a22a0765d765745e798c4b36065dd6909bd53261ccaf947f86828a272af37058d5c98ce0ebf82e4cc4946f9c86eb61164d9258a49425ee38a4a9d582d55794c75fd1f1dfcadfe9410ae3b5681a63e415fd249ba03382a686745f86348ea766bb1d1270448868a821154af9dd4cc94db4f4a45441a752f1c24f4fad3884b2eb2a2439705c65b1066c115631500028c0cd86707270ba717371aec20df9f05d78060e3fe1526902310fc0aaa7c13268b6f18d9eb7c17a118a48dc7c0d790323a547b4410eae26986ffba1d06cd9816720f7276a2ac68fa99baf69f72c2aba2ed4a6440b4906a8e644069393d0cf4cc1dd465060b94f494320cb6ab422774aaa4dd20f5e77b198e9de8b4f5742f48c27f6d0ae8d2bc6299b613684d3adb9b8d0b7b7f7e51bd0420e5e08c658c6a20fd2dd2d548b491c4e436cb23fde646a78469be6226ec235423185795c9c142ec81c42613453f0c74aa71f6fe01956a7120c4d0d45799f28bc452585c917627de11309154a34946ae11fddfd743cc463f8b8858842c99f42ddbf093bd2968b34589ae294803cf49df343200768110fd1717371be7274723b990f7b14d2c99d45135ca9f0802f2c2caf6195a11486f9b8f5f2a19a4104d45a495069a2d441c1e9bfa90af0bdcd4c4ece6e744bbe9507208dda875f86f484a8ae3d2b73946d7b97280b392efc8483f3f0a4949c3ff46b82030095fc7323d780fc42274cb45b81b56299ff8b7f998aa2233d1823ef7226c5fabc9e6e03c4e788bc041e792833981590d128d911ad25be5d02922c6b380685b9c5d25c35423e906bbdf4ca6acebbe1dafcf6be3c509e3aeee7745dc9e056a7a22b2579276ca2ca1198d4cfb2ed8a3b9393d57e0229a584bc411f6d79a03839a38d81915dbda19ec0f743c0703bfe3e06a3be17cef37254abf362ab4aaa9a4d6b87f05043319d1e3b6eacd5727472c27375737bbf7f7a37e7c42ee84a74c8b48242be8fd65c54f391168f6dba094f245ae8248a0621c4cce42215d684b8cccc05b0244eb02fdcb824b7e18204477cd60b0a9ea561eab2cb392579e9adddb22d833ee7b77265f19535a7ab31cbd415426e5cf385df5e5dd521c4ff8b1c97ed33f75afa82043df2f5f3ccbb79b881c93f3dfaaf740ffe3710537382ff842eea987ed7bde010e049bc6a975671ebff5584993582aed98dbe15fa125b2b5a2f98eb293f282e1b70324c6fb65e67a8f2c629a5f2b48dff16bc323bc7df4c257464188687ee84965d31b27594f944455070dc0fbe4aa3e0525c8b7f58f30cb130baff6abf5b40ba54734345b62bcd4d3563891bccc3aa6a28e5d9737573c674767419e55da7cf94ef3850ba62640a7a4d4ee94477789061824c7c2504bcee61502d7adaebeddbd8f968a38993d3064fdab056902a8b665f7b1b9f9c4e1f7cb232f2db8520cb86d85071de0a613eb5e72ffd38986aa080b1064f7394e846139c7c64a007f454f51f463f64f081fb975a5f80c1f9cd0375cc7f9323eac4bbfd55924ad2533f8df6cf4d5dd1176c84632bf259ffeb7ff63336d43bc169d77f795bcd9508247febf1b8ac1001f8eace6920b0588b07744561a07458bf1970474d6513990c538de63faa8cbaccae8e4f8ac6a382bda2ce1c962bb3c4736033e9654797d4033a5f76153fbdedc7bfba1fbc1fafa0ae3bb2c3b40f2bdf1303c7949417565f2849f9dd747674ca7577757862befc34105d5b3b811dadb12d8c3e06d1ab1442cca8d0bcb65d740dea2e5ef792248ed9bdfc35888bfd1691739511198b663cffd2c84829d254f1bd192a8d953373d8fad682701f5015f904c92ab7f705123560e1de897fac76b10fc9af9bc769a6dc019ccbf128441cce73da2329b3a03e0bcd57e408ab2569e681a23b62d5c65f4dbcd9bd403c8e1434ca88c245dfd0ca70c87f6be553552d6e6c3984b891e37523a74a1350ae72b186c38d52eab0d725d1534300c5ef7d3ce4b52ca3da5b32d5fa7e2a55c7106d7d9bec2a46dab5cc666033dc6fb722acda52d83468d6fa2578ff5267c9fc827791fa9a5b156586ab153f3f5a46888745d2f163ddbd9323319fe5e1757775ce76787646f05dcfe53c7d29e47f0ba29eb33de50136a3c412c85cde84b8d670886519b37b8d31e8775bcc40b70284c977c1699cecad78b518afa830ffa283853d7a6ec8b7f651d0f905731715b469a81e582ad7e72098b392e8fe193f48e6b8b3fa976ee3692965134303945273694ecd00d8ccadaf4dae55e77eab9cda9fbbdf0b0e02b7cdbbaaef120d9adb5037f4f491dd77128327672e23e626589023f01c7f781ab0a55f461f167458b87a61b6514e23555d968742ccd7c057c1b1b1e6a857550d18ee1727c5789ec7f84fc0f7eb62b7b0acfafeec6dcd211243163c2b15730883ee9d9a1d301465e098a4c5beede270f65241d3afe5183088775ce2a1839f9abf216f20f0e5767876d27779777b9e6a473dfea1527712a33254ae5a3c24c7890301d868627fd1e698039fa7292b1460b1ca3ea0808944a730c1774fcab93d14d66d9760e22a5c8c3c230f92b28bfb5bd300315dd2468721fbbaa6ad49a8405a5d4957bbe099151aef8e061e503ae1e5adb2dede7de08a25e6747b8ac1943a05940e6494a53e253c673939d82ff70438e6fa712f8df6f08fa2e933f5ee8c7ffcebf52194dab9414ec33c027e4f84090a4c27a16fa8526b54e9a3499001439c66c85f8438b6b7842a114302faa9c859bf45bfe49b5e839aa7abf69f9fdf8ebc25c050aa2a5d446c2a6e8353f8bb9736463dda1226926068cd680eb1421da719354a8016343a5620d172babeaa59897cee9be9777977d6787a780c0891e2e773d15734457b63f37b9a6a496263cbbe340791581e7cb7c9ef9d42159e65d6848ad84703a80b704989540f2bf158b1bb394be1e359690a066a9def936fd064a195f571d6a89e6e058ff32c4bd59d655ea57ea91b7278877f12f25fe7332b7eb3c80f94c9c43edfbcf4b16393cd31198fadacb3464e3ff62059a57419394afd727d23bb38ebcd7ad5334d2aa73205ac0d4955e2b846dfc1c00e55ed95e4ec7a4ec4cda397849ce9d4ce079c160b4af4b875f3ff0ec2e925dcf98f75d85e6ebe41ecc25dd1928bfacc01c753a6b2affad18febac4eec3b7ebdcd41a6f9d85cc500256e9e5c5f860483e83a54afc722a8cbfa55a5bff60f44109ee24088f278ed86787a78daac791292fd7b79b82a794c9a4348952c49a3acac839551b1c89d8f6fa328565c05f2cc3b84942d7a19591c0598ae882f3215e978a0b8cb402ae3438690df372f34ee1dd5fd036b98e950f89ae393a6d9b6ef9523c6561373fa1852794709d38d03648818b96eb44c41abe91cc63e0ccb7ddec60f20013ca7547360bd0d178f39c7fcd5beb1883bd9121e4c66072379aaad9352200c2a2c0ec15c22197f57f6d3a662773193ae71b9bb2d5d8b53d8bfbded9904ca85bcc71c41c59468104f54d5c88ed7f7c2478a7b1190d4aed428d243458223f3857829b316f0c9a73a790e6fda8744f19745e90895eefcddf787d094e5e39648905d523d1828e83de8009ebce19a854123271e7742df27f1797b79de7a7c7a5dceee222e41da92ad18ba6b2b9521d37e6e61358c29f92d1e85453cb0497f543486f7768aed3ea8ac761a96d1a03fddc5682bd4f1d29f7cd3a8d005db3307b50d98159a6d74a5fe7127f5ba8e41e1e542abb038b7c82e4d02c11fef4517643dcbe2ae1da1834ebb9257cd324bb4680668b3951e7826132d9f45b92dd70fbd36d9b2d0e6b0d30de6ecdb494318dab1fdadd9967d97ba6813f4deb31eb23b3b7757882dbf0bc32e1d6bcd4d3d3237ec8b85918d9e9e92ad712a1d3e2315b1dcb673261107c68fa64485498407f8d1c4d420b5620f5ec1861b77dc9c331a18fd0dff363f6777ab98b8de7ec4f91730a22abed3c82ac2990752d3f0a22dc363bdcb856d8eee97f57a7c7ae27b7d7b1cddd1471a48f940547bd5922d090039a16d1c8bbc7ea3fa8fb95220fbed8bcdfad53426f39fce5aebf439985de40f100a9404997e8363615c3d1513b10324c6b9409e41f84210fd3311858f8d2bcc4a40872b8d914459c39567598adf0b464101e1cc0f773536d4050eef9ab4f0c9db8990b5a4a23a05332075f2f0f96b26de0456e3175fab082686bf7f1872cfe3c0f3721c2e59a50b73a75ad2d621a76bbdaa235a4a5efd7d69e25b645152301cf2e629f32607ac17afd1149a80d11c3a4fe320f30a83c38f34ba32edb701c9b207b608d370b9b9d1b3be8e288164093c410f1a535b172c5949c8cc33a3b1e25c0ade15b88136d1e25a65ae09687308eba4e4d428dbf97b7d7be67c7e7c60525fcaf5da8d45c9d648b3aac922891438a3ae1dd00f92f918dedd3a1406b2dc2ff79523f8c5c0b442311d03d50eb8868fcbfb658f8eaf2e94c43cda5611058abe8430615919aaeb7f58f89b1fe1393da1b78f0aa565437888743d5f7401f7c02dff3a19cc2a3bc8893a4b55cd3000b5675d82e2b659c9b99bc78a6b4a8d1bb44dad8640e2166173ab09bf1a47f67fdfab6bd40021736a4021d24b8a854002b1453ac2032af68c33fe7a5a32ea87b7e9f1b25eb42c2278664c13d6bbe2330599b9e55e831a5cf0cf383375ac09d4f5a1d0f156066b55d1639af1424da789eeca524261d67e606006ce6ccc4c92b21af526862d2ceb285cfecd60bc97780892810661affd867c7e7cea7d7f7d5ad75418e99493083abaf4846ebc7f93f56b33527d953483c87b2a4e8e6ed8417dea29bbca6e604a6f4f754f6c4eb4a4f84e9e91abf8ca5a5957994cdba514108a98d4bf4fbfdb2378cec25232ff8958136f4fc62dc738522e99a73f99b616cdc08ad6b2e56097280724594a62ee1867937dc0976a9d549461664cc010eb8a3a64ab4448c5905af75e6452ad77cdf7a66764108c706460f608d91d2c5493dfd0a4eec86c12af836416002fb46847c10b9f7a1491576881b8f7487512bf787741c1d14df17eae403202364c322f5dcee58c52d78f894fdd7a083e92a73f4cd26c5faf8564d949520444513b2568321d92608a7fd65ed9523e545dc81ef4f52faf1e11a4c84330f18187aaee7e807e969a3b815854d0a36e6091872657bd9f15972cca0d8a0dab3a7e395571faaeaa319c17dbb7b1a030b5d9f1b0a8d367316fffab96470a37954f4fafeefdbebdf9f197ee19967656c08ff651b1f82dd6d61e871f41cafef5860d1e3f88daf2232d097418d2a534b47bee95e65de5f8ed1d4b83ff3c221f2264e51c0f9f6d8da04892dde53fb5ab962a9ebdcbc85a4dc983d65f2812927f67ebe67ab1f01130af635299a9394192197c531f50d10207b04a7574dea6e2de745e72fae2773a46302d88063bb65424004628a5e35bc6856a967799e07ccc382c67673759d8d7de4fbadc0002101827018312cc5518bbcf9cb6fe25fe511207c62fd71c537a08ece02d5e8e30486d857e807ef27f817faa18a69b77092461d497241f44fed80a73395167abda443a846db1beaabfc5d0e007be99703890f78983b6a7a41b2f02b3f78e12d5d82bdcf837471a96bfcf8f8f74fe606db697cb01795bd61ac7411310c79c91b185347ad56c51fbe934d4d03937869ddb116c5ace0765bcf73b09a990fa143bc1a58b7053c800f8e7af9f3d523b070127fc578a0f6e067b17966b5a2066be52501bd707a828f05199074460b74e3fa43fc88f08a2bf407a783bcf595c4e33b81e95081a16649ee06dce4c65aa5428fd0de3c97b866730c953499b35aa6be61e7b5af9d5012543dda42c53ba1a9d7f6b071697329d72a669592b0863d8f7d02279689894a6b948ae302b632cecb7d61489877f817ff68082809e6443ff255ef31eed3c71984ff673f2237adc2dea7eec1ad1f8a985c9182310743453d7f2db8bb6024da2bb9e7316fbcddf8adbf144e523f7cde8040ab2c229aa00c43a99794a7709163d9bd066447b8acde86cf628ea81795fd8b8264a84909d3c726c4238bebd2e1310f11fe5c6d9815fe36a0556ed9b5ed02f8bf3dd1f0babdf3e184e0b5d2788ee098f86dfbf3e8c93e7862501f538a121a3151bdfdc1a89931f2ed9f01176356c2f3d06e1e7ec7f1e3a891ab5e1cfc5df5140effc0b5a773b85753e59c82c45186aa6d1b98ec891ca570784666545ab254e5f6835053f0d33dcd32f5f21d4216c73c2d7e99dbc35abc52b16f80033536bf0027c486186fa9a004a8d87808280fa818381c433847e6959b954973143bd76f213e4a882a11f9cff6124f4d37cbbd7af2e9bf483a4d3f87392d78499b13e0c8531bde7a6824c3db75928c227747be79aea13acda883685304dc45a5c01b9a80ab3a773f83f11bf748347f95d5f22ad5d43b28c4d677406371fd3d594bff57f14a0e7b8ef34553f7c9e73c26443df7efe0581de1461a554290a51377924465369e7ae28e81cde9943ac70d158c09c1dfdfb12a9f3a52601bae7e77f4fe396d3fed51b08941f29b8f6fc3cba99e8baacab7d4ba6a5f408bdcdc0f833660ed84c8b443c956336107511780673e8abeb87e253fe0946b0b7b830f21080e365eaa4f1ca9d91421890c0d177f0ebaca9e722d520af1e11a43fd1f4889187aafe8284828a495d97e9bca05dfbe686cefeb0b0fe7c2c300bd3587dc9eb293630264b1a5051d2c553ea4e498f72d22e0d5567363dd1384e296454552c7aaf7e98ba79512295e7364c9a88a3a6c3b12c8e536bf2705d720fcef1f3fb9bdabd9d81a5bb19fe4641172ca0ce46029131cf15c3500dc58a8f2a14215035a87159e14972bccc7b7427ab164136a54b89e7025bc4a672c9f58e8c19b125e09c1929c2421b495968ae1cd29953a01b118fb7423e34430a7e64f34c0330862d659c37af37a99d8a2b4c0a69747e9655e11483d2042334a014a47d77af752a9830ae38d5a09bbb52996f6df5f48d11c7e0e15db65eac143275271e964caa0f35d54e01531cad32bb50687b0e958284828278838583ca7b9787cc5a2ff368795b31348dd403a9462ffb4c6604b068dd5ecfea5f5b8132313a30376930826a523310582870689be0dfe54a2ba0c3f81735a9e4f167579aee9c5789a7b2f9c567848fd7299ec57f688007d7a8f558b96d60c07bd476bba5ceb1656c403e95ae8303bae39a8568eec3191629add07bb19c911f73f75492c304e4b3328f94dcab37721ba0469b4e32f9358239b6e0b5cd39aa96a0a7f3ee627306a3649bf570bb968766d2f488c33eaaaae1fe82d43101c9962c70c5e7f63694e1a8956e29f480a4a41f0a6594b57eae42e9a4c5199e061395e47d13387bead2a3287fa1e8562d67dffc59c9e00e43454b43830586f0f26f98c957a0bfa8805538cb87e75de299878385838684868481415201b75601fbac09ca320d18fe664265344483396766510853292f219f39f3f2746b3164855b76ede494251e74edf0e51fc3229ec2241d9dbfb2dc4af693866fec71a1552e5a1e221a17b7508ad2340e2b753d19802acfc36baa102371c11b0166ef821e469f42f33c0bc7d8655eaee899b43f9788c676b0f542dcbfd416b5110f9b71ef56000e0014826ab7f06050c8dc17f0e8a891e492241f817d3e2a932c449e5a8066ab412091e0f38eeb03eac3d981858d64a2da92f7ba957e37c55a42a8963abde104aae92907bd5dc2e9a4f293959ae21fc6b276c242271ba9d233f3e6655b47e12c4582b0076b205ec0a7745352f6feccf8a4e5169c9d61bda72f4f505c9d8486848a858785c6c49a6726b5f5e494eaff2bf2c2a6d886be8c0f98f679ca2cd45811f2003b46b6530218e1fbcb5b0bb09b42244d7d31c8c17c6969b8718642b5dc5517e9b364afc66b4c4bf9827de320b159dca4cc397d2fb6ebf8f8ffe117e5513e9ececcea317e1e5fa8fb536d3d344abd834ea1dcb30277dd2ddef6fd01ce8537337cae168ee69ff6ba9635120f2317aff9963c523a631269e97cb905c4d0192c7ee60ebda8b33f39f4b4079de53a96fcb71c167a291849a7eb48d5c5a0b29d95b205c8f2a4593cd95ef231336d066dfe0cc7156192176536cfa62a2690c509f2269d6b2bdfcf242f070273f8abcf4c7ce123db0fba7dd2090f6368763bde6cfe62ba79a72379c4c1a1878587858e868886448886e0d070aaa87324d1e652a5490f734319c7c7d9f2b70dd801bc2ed8991fa692f2a83385025525a7ec4618c434ed5580a80793e7bcab706356d0c86c2a8ab03e97afae0081c3e36b56ad3e6f8ad4b519bb974622c7f2b2bce47b3b4c341db81d52e001283f429efc808566cefd37abea56a130ff84f77d02eb9ed5b4241aee0be38ccf82194df12800beff50498e5aa94ff91cc02d328f9c8f34792bb7efb95b23126f2276589f80cd2dddc72d8108f402f53d58cec9d03789c2f2e587688522dd9dceed5033645818aee24fa02d8feb6f03780d2a37b57533ea5a1ce6b72e101a1a8a67bbe1fadba0956c05cd79835352908eb23398976c72f369fb8e530b514b6da586888692878987238facea13bda77490323d62fbcab7b0fbb2b47d896bfdf70aa08ce4cbbbe0b8dcb8b6dc8bc650bc5a2c67cf0710616e2c50a7fd6e2117cec383730c222a168c931c055a06dd2a0edca7d6c0eed44056f5f7a7ae9244314ccb352a3a3335287e63d30cec87f2d8add3ca8f2cda22de2a8cace2d2c0e6de3803e214dd253839233dad285b4d4e170e7c259c8c6198cffb203d3472704d7555b446bb39cae8b61ea9fe65cd4589b90526a57004688d33cc52d3cc1bceb130d84b5409a7112ab77aec2d14686c402499d1695f82fddaf2c1866dd9399d0ccbe4c5372dba8a83bb19884d425800c2df536d1477c611a513f0dfb009814df6a3ef90b3e2e82d37ce7ec708a8fca98787898796888a889cce4c46fb6e5ccbf1ce34f036499eb11ad46357bb7ee25923218a7b216c5573bb9ddcadc0e5e2aac584e5e71338760c96d5cd597fe0c7788cbbba3a2d947957b34b5d8027338661cb44a08a262c578f684802fd3703a4201dc09a3686bcf07beac496527f91033d0f5856bb6ccaa21db1d2dc2a3729b980cff5209115c98937893f236b3a131ed8bbb5a75dbcd791b71c124ceb4a2a33268b53d42a562e39b2b6e493de38f96e93211d2f8efd3f902305f6255c577e09c872e6abecd3c294a676030ca969923366a9cb42edf8ac3964b7f95775e9690ffcceb4b35ec409abf631cfca0a25610a16ee34754c2e864e876e763391da7c3435c367ef1d9141ba77e8142452ad87888a889a898b89190b5edbbfcf4133fdf22d970fe683f54ec384048be08a1b69ad387b98d914a55e562f6ca8fa30fc36428c1324dab8cca85874c8bd750d19b87d96a71db3782a8a5b59e6571a1dfd585585f03cbae8e98d07512654f0f3cd14e56b1f42bf4496554ab0fd1b82f6009547548744af7817aaa5ea718a2f15d7c177a45c3ca308cfa800e5a17dff81f93da98df5cbe6f70cb4cc1014c6f5ca4891c10c118c33869f91df6562088ffc9f32e081d7a1405cc2a1855af7ca78e6706f6484e669dcda5247682d25966cdef257a819a001e0b322aa326a5311058bf6944d0ca91811cf1c7909fe26704d91c04ea14533e9559234fd15fa1450cc2310ddd586cba398e03a5b6aab49b1898b899e8a8c8a60ec0afe935806ee71efd66292e8b2beb1ca536f6ce948e218a37cb0bdb3782f5510be2c59dfb0acc3190fde4736625866eb19366e3c8f0816e087f13f14c1f4a7868eaff71c4b8ec0f18252197ff30370e5331431670e3919a891f3a0928cfdf4c248516e9fbf5a17107e89729bab758c75f9de39b4af4dd9d0de313e63485a3ee761d8c409c9e733957308d7fd9beb89024887d22cdcc84072bcd7d37dc39b90f4a43a63ce8a0877c4b2d9bbae44aff08616b2ae667fb26579229cab63b63dbafbb2770f2e2274d867512109db9fb8899df8ac5407545f3f4f258fcf2bf3f2da6980879e7f81b7b0a0d64189b22bdb77b4298f404930374d2a15862e557386cad044e3b5878a8c8aa28b8d8b8d4711978fdd3f477bb39dfc6447d7c6dc25843ee510623ae1dee880c0566a2430091e1be204c8abff4b9b5e41f61bd56248eff88e5890c6dcc2e5661acdad77930270e59927912fa79b8339aa770b193181837acb26968698cb95c6f3352f40d2b547afa445212dc3968f66682f12039893ab104ea7745a8f720c2dfc16c86d0bcb9c781c089924eff2cadeb6de0d2a960afb2bba427c17eedb26e3c688f53db1c7d6226d095a08abdfc4ae20f5b6b68a32d9f6c257c2323d232deea7b0ad68f7e1446cfb15dc83054ecc57999a1fe5942c97c46e1cc7e78600c534af0e7fbeef9130498bd3080fd03e22fcdc6e630b40b50a15af7dedcca96cc0695f1bbeb0902408b9878b8d8ba68c8e8c8880c2c158e6558558c55abae69087a5690bf2b21004e610585ab7be7cf6ae2fa0d75f4502832b33ee062e73db6faa8ac5e0425492b048854c1bbe56d0281809a2ee1d467ba6bddcaf11f0bf97e8a48d3580a195d4cd692b952c6f871f36dcde654ff3d42e3da95155fb4dff141c820985a38f053708dce954acf004a4d9a0694f96e741301f3bfd62c6e3386b1ec1d1fcc33aa4bf4fbf33f110567b2ac4df2e9934b278ed9ca55713da45b29eb3deaa7c8238f719672d4edb89e3f44322e49734616446abc416478a39fff4aa9982ccb1574e274955996659af43085c250e446571203103d7798e27bbc6069531443a71bd3ee73a25c256259874690edeafc9c3eb4cbd878c8e8caaac8d8f8d4242160fc94fc3bd4bffd2afa5da77be45de8fd12d865a160bd877b823c8f548313a32f95ddaa8e4f7428d916f72f6e20b5fdcddaa7f720c7e9fca25c471c612dc763eb417c3476a114b54d3b555d53e2ad5afebe1a218eb6963d04fcda6627bbd5fc0ef21701bf19e38131ca45a3c7fce7f858d29ba5cd1b430c282218c551c8f7654c9e19eb0024193490c806f4e660682d78a788ce46ebd4107e7f198c027633b79ac29835229c9b4e585f765979cfe256d576946175931b07ad47bbe6d9abdaf0747df90ee85905f59d9133158ac034c91abd1fa4988df47a87d13dc8f8e6031ac10dd9991d2e8d9504338b4c290b27baa20d8e18e6234b0b7bb0efaa8d12e1f29702f715ac1878d8f8dae8e908e7245c28bce49c44cbd3d207941a3861b2e2e1246ef549ce864642f4307aa2f5034541a3b736a0ae9ddaa7f8a73288e3b0e9baad3a5d7e85870c7822a42dea13cb94d295781d03d5a1f393a58fe8fe615e3d57213df1d8aacc6fffa97375255aabe6f42cd9cc0065a6344880427ee7dee8144dd7349c1bef576be96cbd3fdd5ab2a93b96804de21013fe8578d8906b181722905ac3061df71138e36cdb8f2c006a0f35c29d62055644bf959911578b9079a24cb4602ce354ba7b25de839edd925b072c1008142500337af8fc734c25a89af052d1f9ff75246ff0a9cbebbae147780e6af1b7416ec260d0109992304f64fd490755ae2d997535d6a73bfea5a0da57ef369731a010083eab8e2c115804008845ff0a20f1240012004e00180022880a06229820000288e27080382b022b8e800cce50033e0e4c2ec436c90d55a070763c40d88063277a1d0b38c6ec936448e09b9dc03d041d91f282910b922c1fcb86ce1fb88720f38c9845c35c7c78b189c63709d43c10ea09f9cc47b403682038134115f300c72adc5384fc28bfd3ac7a0f0457d29c12f9d84bf588dd9e6021968068c2ae44ac65a62b9179b39c7f0924e0304005ec19d44762be45a86023fc20b799d63f0f08496c944d115d89aba725581b33872855cf990e1497a81af730c0cbe3cefb8e013b889989d0ab98a28f0427ae1af730c04be088f18e02bb849d06e855c6554f02dbcf0d739060e5ed0d2994ad40ab4820b11a53a17701472a5f195edf2c287ce31fca16cc565c14ae03282022cc8e51186d56f2f62e81cc34dcd89c307ef4db9def42278600e5c9e8a902b855717b7173e728ee187e22628f16dea6c0bb43c1a065a401028e99073f2153f5b7ce71cc35c276a637e21e0e643e7868c5c81505710f702135d8881be2faf6658f7b7e0cfebdb18083e845b362ac2e3c8958b57b6cb0b1f758ee186b292ea92121503a781ab0ee85a3d7265f08b85f6c2c7ce31fc267c053b49b70880d1f48972f22216417b885c83916b0aa796692f64e41ce38fc207c505e7e2bda806a72120afc8c509a234a87ab1892ec4a0df97ab3dd7fd3194f4fa583e118f9640567c5c23d77454a3c3bd90d439862b691ef8040a16b9f4c56936f831dcad789ec7912b8f57eb2d2f76ec1ce34fcdc5c004db8db924f5080fcc06bbbd8ba11cb9f278f54679e163e7187e524e4c87c1393e3018b872acee16857e0322128c5d24f42c633bb1b94e0ceeaf3430fabd2d90f3700534426c6134e44ae32b5a83cdfee7f78266e71856c4b9800af029103527ea2de021204e287f3c1e72ad609c4bd28b7d9d6350f8853e85bd2dec411bb890a09e8f8d5c09bcba5cbdf091730c37942f0a03dcb75c68f2080fcc818b97fce538724de0d64de5058e9d63f859393198e00bb8f9e89c21231710d612cbbdd8cc398697743c620a763717607ba4e0cb1a23b6f95ff0c895c05bb6db0b1f3ac7f086f242f180efe0a6a1374147aee0a85b897981b9730c2ee7e0c610bcbd137dd7a365601a480af3f7e1916b8012bea517fe3ac7c0c08b5a3bb6c9b503a33117ea3d2afe668bb17bbe0630e48213c8ed9317f1708e81f009f635003faa9a5f805c6ce382ec6b3046cfe1c895c7abb7cb0b1f3bc7f09372623acc12f581812383840366e2566dff13f7c034c8d885427041065d7e577102d02d077e03171f0edd0bd57f01670a6e93ab06ce31fe0a5e700c70ba74f75703ce80f8ba1ab9c0b0b698f5c233e7184ce4d89501b8f548313a8f968119d00935e462a2804bd28b7d9d6350f8853eec2029b88b800e9425572e5ed92e2f7cd439861bca8984033e839b05d55db938e890ab1c6758d25e2ce81ca3b5bc9f3203b6f7bd580926d14955727151f1b7f4c23fe718183921de71c17d7d733c96270c908d7555c9f58ca483a7170e39c7d0266d83d861dc9f030c035784d5af92ab1605be8517fe758e81c08bf28509be809b0fdaa992ab9493af0e5e384a0b31d02bfe2fbc349cf637d8dfebf81df83d228cd2d11002835cf00298bfb40bfd3ecba0f0447d01c11170f1c3027241ae3730873a0e07b590cf0b1a9c637849a7012ac06de100c3c01521f46be4aa45812fc6bcc130dd39462b82a3172ffb31f5a71553d76d9a02b988027351ee5374a847ae24bcb2515ef8a8730c379495942458055c7e50a046ae47241943265e08640b31e8f3c5165bf216685cdfd942fbfb162845ad05b00d17d4c8351dea288d3a2f28718ee1354e8305146ceea6b88673850572e17ea947ae4938a5a7bc9051e7187714ab544950055cfc00a046ae8fa012697a81a0730c65f30a8818e02bb849cc5d2b7255e30a4bca8b059d63f496b713c528454b602cb849ec3db0912b30da51625e60ea1c8397393819a3a0dfb61d904f704adc90e6507c1cb9e6e296a6f202479d63b8a99c48830959ba0f287fde0b3560244bb363e97bd6c8c54205b7248ccef32c83e311356380507025d15d8216b98045197d0646649f653099a3400d800f8a6760fc1bd09953d70a68841872aff0869d528057247965319628778ec1e51cdc18823be0bec7542ae412c49a12dc0b4f9d6330910777dd0ce8ea20e2290ac32530c1fc288f5c91eb0dcc61a8f26213e7185ee3e89801d805f723948361c8958b57b6cb0b1f758ee18672227538a0cda90b08e573211ae462ef7b116b916bd88842bd0c015fbcb8ec490c071b100c92013cb80220c5b28f918ff1ed7963075855267baa642214684f115c342c950560d797d70fb31f8f887fda658953eac2f3be6b13077fbf4fc77ec419b329ac9da006db9076abd89877dfc2a658339a6b0de52a9fc26d07ee77206e57f8648478db10f1cdf1cc49ab64ad4f73f7662946504637988c87133cfc4a0f99e729af3a999ff92b92175dd75361242d687cca952f6940f320659750a7e8124e998dc7a99d82f14b36b400c2c705cdb369b3e203391a1ed092dda8c1368cfee05270d4bb6d51a9e6406028aad695c83412f740b04c9bac065489146b874e0c889e8ed216b85750a7bfe83fb6d5777b699573097e5410f789dfff1dfddf44df4d88353241909c11b87c66c809a13aa0696d71e4c98f918fb69092901492c9f22b8975636a8daad5cd65c38eda7b926bfd420581fcd23fdb2233b36967592939fe2f0aba8f06566e0fc35ab3bf40352deb0754108958bf448b303a5ab6595129c6c0d88b1c399e8c5ad5fb7c8bd1c6ab2609228a2ee0797c97117c2f5c8c338ff021d3e7deee933c8f7a46b580d5538fb4c40f21da6ccba03014e5106117a02281c9e2ffd564c8f94ad20204f197d9fa6b017f1e4b6d0a87183b660f90373d3501bdf32168b5605ff838eb6b749c924d8b19c5c47fe007a92f18075b1474cbaeb3914b02f24ece063d83d84b911d0c5509fd334bf076e5f97c5e74fa281bcf9be8d489bf10295e4e0564098bc810f3d02e4f9fdce27f83a57c7c9e8dad74e1d2cd909290baa9912b54190193664865696768741901458322543cc2e22dea83d9de4fc100e66fe7d9298ab3dfcd132f3e2129a2eec53231220dc6eb348331a1ef91f0992ba5d5abd43d6d7d128768c479f1bf6bcb72df25a36967836d9b4894e0a57baf73ca4c66edf0cf59c7945021f15977dc0333edb01f89fb00883efac620b77e08f95d6bc3ac69dc6b190b20b23516447f14e6ad767894bc27569f132159e4526b2ee7b9d11817d807dddd203f231fa94019d03c1f8fea2c37b93402a434b411a9364687347f872ef36d9f9904919b29fd9dcca44576dfa640e0887659c4cc64f69fac7284a3c906223e7afcc2304ad1631e0f5b4cc38e8032ffa86492a62b86f187424b0be199d6e0bff93404634a4932a5c9b8c008d815d0b26e8f98d1a8be929492a1c8d549672d4fd9de8763a54f90557bd161d1f8d2270fe94713dcca3bd6bab9076e592d95442861eb32c49eb87badcf92d311da3bff9ed04de8f24ebe367e5fb5880bc46f1881fc4b32014fc823719ba8514e2707756a7c5a81cb2730fe50164b52b7a7c3ebbd88c884d81d5497ab3eac3c0221edce523c442d130e6c6d2f94648d7e193decee021db59fe5697cc73213e688ecc008a9c95e19d5484522201b92d92965f85b1e00c9e1292d84750d53a74ab702258b4c3ceea3ff6dfc4efd7c62ca7536578b9fe788aceda5e865653f96dee3aaa9a8b2c1d2ad365a4253ae1937f06176633f751183ccb511250558a8fc70d20afebf409d92c4936f10c2f3d18a45bd73d5929492c2939593d45eafcb97fb0c4f92eee74e550028540140c7e18e222222f685c587fe12669b0091d3a8746b36c096fb2b1d6b3c0bdc76252a2b55a6a52a5cc3b947734279fb93619f4411941f7eeefdfce1142749a5617d66ecbe199c24eb0f8f3b3f8862191400f626af10be814fb2b93d250c7f90a0987e3ea41b1bccc4ba7f523af50a4a8f6a53bf4ce1ce269e042366bd5489489c01e12bbf4c29ad86d96851cf6c42ba840817c9d3459a081c67226d963857a4baea967bb3ad9e8df79588ed751e1b59fe0be80b6df2acdf261c9f11bfc7ec82772d057b8f4e74159725f9b8866d8bbe2ca45ace94b2f42f3971e6306de6edeab1a7d85f1e804f9e5196896addbe15b9f8b6f8d987939593c6949694f936d8f44f261ea4970df460322cd68b0f90f6a045b51e07addf1891eec0ad6b4b1726b7196d5ba7feac44d957493e520f290ba5fb88dbf2917adb6ea029cf46a12f353d81a6a05476246ddfd916a0430f8a8e0194a0bae4d1c865b8c0f1b64b3f4e9e64fc3fc32cf79535a84a50fc1eab7dc5f54caa1065b20c1277a7dcaff8979695acca9afe263a73d00fad698f65c0c511964cf98c8948d4b304c4c52beb89d10e5f8498ad9ff0f64c68296df3f0c35ea163fd5c790903c7807aa86a5bdd2e9353bd2c7e6314ae15b52c95664fc1ad61c399bbe8e3ec7aa54c9d5983c5f010314ac20416906c3f8b822596b87c8873ebcd337e09f87d619f0a4436986642028e4e23dd87949694ca95979563931a5e057d6a079d58f3ac6c9544de6efc9ddac1f0f006e6c79a71e41dc6153407a3c2fe873bb5fa928970a858887730bb13f69cb9e7057757591242feacdd34b92d84f8b4d7c9fdfad097d234f48f519b172d52fa2cfa4cc84080658b987ab7d1af2c84489a8b110734d13c2b95b9dca3ea39869f1cb3c808aad1463f475d3f81990d9ef2eceed204d19e98e363c966dc94147a78f5e828a33e9b77f89e26f180e726babbe88fa78964fc3246b7b8ea478a86a4f8a661fa4d7882494093f8c1ec60ce82cf6a2b2a1c0660e174db892b81595af96f858a96db7471d1829e8152fb1019fee0f12ee97539a784f6c7170022c965e422504d8328af0cb8a08b0f7105af1e11a453037de187aace9698961ed20742cae6311782dc64929ccd2db1d4fcbbd7374b5b8994a29d5bc184fa60c2a97a1b8d07cd58012d90db5c140b346c67e34cda2aec2fac92e8b04698bd2f8506e9f9d97f81efb8fa0d11d72c5efa4950b5b356b8ae3ecd551264fbe6ef7fb5a82954c614d77827a1e9484a1ca4348d7d50447034092e4a8ce2c73453db67907d189ffa25ac6bc1171a24693caf44913896893f70c3a680d5ae133aac6c52b2f852e2b5807c748cb88b1f70247b6387f7249643f4e4fc32d77bc4cc5b5b33a6255e0f87d38385220fa4473d0e3bee85152b7545373bd9a5cffb42a0f61f2903e83bc65b2113d420698908e597be4cc59858f4d6c00ae9dd96f5613248a884f69cfc08e5969896d29799975979d268e6eeccfde047ed639cfa418754a7b061abe582cb633250fdfac178d3b5bffaaca5e85132598c7e45a50ddd543be8fed86ed83530d8d671c0fc9c3aa8d71fa4897f53d43fbd45b443689f58c884d1418516bab0d6fad3f6fb8a15d6145eee6e73e7d7e0997154dbdee8a539b7a925bb720682ef1eca08ac529eae82b62f781b61605f6e7cf3dc67a9e8e453a5822d8b7a0007a4b093a6b3d01e9e00a4c430e570d6971b9a320ee6937ae4279980831ad46705d37a215387ba30c179c2e29d7290c0f97fca2fb5292dec6b729228f19b0d9ed9c787ddbe55c93998083dbe06ee31062b65fd791bef9725a44f59e2412a990d1ab7570a089762d0344b66ef2388e9979997d6989a98de1688bfc3ad0b19dba3cce13f70af100803e58e0bdc1520403c5764118ec6a7c379c0f7892a394917a892121351c1ab6854c5d9a438a4fa123439bb6fa000bb9626fe0d35e05284139be592087452b16b1d4651c0f4a4a8eb22521168d18a0b7aac453c19b6f735c71271a8a9509939a6ccc465ff60c9d520362d41299799e6e8fec21bb115d20527593b92b29ac2e5a61a5c92cbfb354361d7acfc8a89fd06b97b5be87951732684bb9a9ffd3fa52f211933ad03a4638acf08cad2fa4861717abdfd74dd9af6614d2c21b914889f27b67701ba0519fbd157f017cae5a0b879aa5401ab050f69fbfc10cc4d04883fd678fd05f7bd10fe1a85c0664b87b897b1700a366eed989a98da999b993e3bfdc7118138bad856231650916a9423aec865f661df24b0b4359fac2c98953743b018939ca716931f18f4f32c438a9d697006ecad655ea8c8ab01582c6c6380d82ff7ac764d3585ba805116a2ec0afae11de7ce9f49035bac1842fe5f35d008714bf68c26702fc262424a16f070fb90bd51ef6624d0b786dd4a7795af40388414df2c2361a6514651204f8383de118bd924bdc93bc68f5722807b4ed4e86f958b0fabbafb0d4e710f80bcf289198a5667f45200915157c0c5242e181e50e62e6e3dbe2c138324cb2e66d08397ab4bb563988a0f9b82401df5138362a97d09192ad57ec6e100ea32ee304899f30cf9aafde5eeedff7fac1f41234655c68675ea2b7708f1999b99de9a9c9a83edee77ab33b61e614f305e3fd60a8b48a7aa1fb8542dc5f8fc0017b99d8fc48fba3088a411fa0bbd3a9de3dba15b6e504dfed94d7f68d1aec0a841dbf6232c80eb6284c3858f377ecc7a182959c0859ee56680305a12b40af73cf64eee670c8f8888badd437f111c4d9f1bc510c2588502fa897b18d1a0b9b6feee10392ed1e9f4b28dd561f5bb0beda74c6f7cf7671191c3f86d6320739e2b0bba302e32b285bbcbbf43907158514d99aebb2d296f3d79698c32eae46938da43a081f532a590e78f866159667f3d86abac9a8f452d83f14bc3406a688fda7c58d1b4a34f18a387ed40ae073abb234ddb586f8cb315fbc1b354a61e22c28f918b2ee29f9825ef832140f59a9c9ae29b9d9ba6bca1a0eb622eb91c89f4d5cf7c9a1b715d82a873849cfdeb9d630b8318d69b75e384e39bce25fe75a2c58cd4939b9fa914e47e9d42bc486cc39d245527056099de244d3be2d257fdcfdf2a86af0898a22583fa3a360aa7ca135cea8ea943b7d1f6e676d94dd044bb69539c29be7619a27f4345a85df74d5dd2784fee342b0d9f69e06d11cb44158d7876c399156d57fa0b43b94e2ca496c1d21f1ce315478eb6ba39b87279946c5c8467e540083a58fe2f3945e9566aae4183cb5aa6e331063d65b19113cacc9d026a176030310e2cb92d249dd0431a351b9a1b0e56f9aaa9b0180d4936091c9df57abaf15e7a7db912f3220445af93e731394123931c7279ec5daef9879b9d9be69c9e9cb3ab4ee99fbcf07e22deb36df345be5a586a33be5a6e21c0b0cbbf4147922bd7d65f74ccae15e0ae19ecd2b2e57ff8b49282d8d1a88031ca9e11eecc42f1954eb897b4c5f8e44c8b584931ecf0b1b7899ad46d708627ac1a46af7937c03204347b921ce01cd9ae7af32544f6d4a9fab0aaa3c0fd7fb208e341c2cec2447e3897e271808cef9964ff21bb2a6c3e665e94f46229ebc71e66f332efead6ef595107aaf8fe70cac65dca27e1970c2c7653a4fbb3e84c70dcd41b6ea9639c04bd376cbe6c3448ff644265f84817553c5cd29c84e5f3624a8550449433566b3eabc02267cb71ed3571b25e18771272a90983e3253ed033b53173c1c2a1a9baa1e22668f0e19d1ffd9c9e9cea9d9f9d4fa6e4275c49e775444be79cd3720992356f41b142ddd1af18627a0fae094cd02f757736bf106440728103abeb6673da1de6de0c21e17d262c6511439d3c032180d9707cd03a1545ce0bc160b554c29471fa42bbc02f7b4a232350c0a1201d5753c592224041d4a95549828038631e86a60d62e81a2ab384a0cd14e8957d15ada247b9504f8e42fcca02e06852b9d22ff617b03a7a815b993752acfcd359c136a6b037fbf93ebc60849afc6270475db397b607101c81c1d601ef080dd436bd03c5bd24292347a88143ddb6c95148e8eeae1d219ac8dc28e50ad454018cdf0edfa4be39a205a6afab23e0c8e45165e605ee91675086182c682f837d849dc27df53b29320281889d9f9dee9ea09e79e6ae7f9d4af5748b8bb30d9ce5a9e9b6d2eb9a06d342769651a3f0a038bf82b00e86de81cf1662be3a87bcfedd90eeee85c724a3bb2c38bdb466274bfcb7a6afe486a66c0c6da2a19509174f78edc036d131031e4d1d32b27db7da22567033134831d854bb262d3f17662d2c47817b379777c11676fee0838936ecf4c8e9ecd3c81ff8b40c39c3bd6be6c3514fe50d46479d52219e207d18ae1308a9c4a647e419155ac4387e8d74f3469f7804764ada2e6c5ff221fdb908a37c0aeea2f8847049236972758ddfaa2336a99c8cb2137422410573193480250adcdaf53df3c371b42eb08b6c3a5b56f5c7bdd90c0243775b9df51ada274604ab60b8004ec8c29785889ea09ef29fa19f6af98fd5189c085b73959736f968a7906dbcbd854177cba25e474d1154a1c5e16e3453b40f9b3e4910a8fe650921044549557815a93b8759409504a38edcebacb1b5c02211cd4e1a07efcff37244ffba00b16120ff109bb356a4a7018d292565030251a3d28a3d4063def839562f5970a599939f8cd8ee9c51fd51003a6ba8c2805476a946c4c6a1f961fbf560cdb0b215388c07ecad6289dab228429577756ba80551d5ddc1a25eeda67b5ffad00548124a3d4fbd779498c09b7ca5194e4a2115c8876d4381017813570c33d3f9975ab0e8a7e257fa1452b3b632805ede32bc0372bf621815a1bd9ceea02221723a717bdd3984695c2dc100a54b2bd9adc650e9c80c0389889fa19ff6a0a2a0369918440a47d15bfb19183110850763f5522e00f43ed08bd2fe327b58c07a5ade62aedb938b9a9de04a0e1972e5cfae7750713be8bba4d8bfaf5be0577b9034990d513f065129c114a81c2aee07e2ab12beba20737b7a64e6ab97b5496f7ff7ee209f30fc0b50b03b96c3575576f654b94aa1496ca2ad360608971533d357e32d7a631bff3e041fe725a6c184d1a44681a16049ed67c1d5ea932d1f77e3c45497b14dcc9e9ce7572484cc8e5e749ceaf9cd5a45ce05f8417ff4a0b049bd62ff6dd40b2ab9fced2b77f67a92da87102d85c27247fb53a74b136358d915160cf1017b7b87d128fa5655432d96387e5eac16082023ffd1c0135153fe8a78ae723552668f0f8da0a2a0faa1a3a152e2ded88f0fdf30397db54809db950f1706975e2402cf6cf29748343e27072672b05352d361cf4c4aa4ea9464af866e4ee5c8fa9ca29af053ce381989839050876b174e39946b8d9a0ac05cde71ae5409e73bc5b83dba9dac873558485583cb3028f9a876e50a723e94d8ba943bed15b6337497d8939409ac20d99613fc8cb4244b40d209a3dee912b2e013f903f0699804cc43915a976ffd51a2b2698d165ca00f33e1e24b833b0de9ec1bf5e8252adb3417a98a07c53740d11f4014efa6832e600b007af8a56ec37dbd71bf4bb5c8971366ad54293f7224c88491d72a439f6c081156ea09c93430261aff9e6ee2ba3c89276b2eabd423541be67f067b495f7331e18d91a1a3a1fea2a4a2e32535a90239da6e67708d669c934e3526f89159fdfd1eba5f8f9b143fb4d51631685008dc95f2d54bb93b539522a60496b56d27020319b55d2cf1c073df1be1aa755da35c65ef8df4faadb5aae2b6beccd8a8e6f0096f0ae5366334db3dcc220dffd828232d82a5704ef7d242fc4d199563934b8c9afa6e8bf68c97d25dadf41150d86c44683eab6a3cfbd7d432376bceecc4d2582c6ab45a3c922aa91621a78424f2ac857d9f5c27d7ec0e98c8c80a9552da102ba5a553fd226f5d36293d9bb2cf11167ca3260b101d4c31b9e8e45ba441218f8ea3b2bf7df204b7f72e9d04c6028d07a98117851228c2bb0e9639aa2c77e7658689a0eedfdcd5474fd1e6e25f2412989588a2a4a28279aba3a5a394febd8543d078b26e0cf44c803304882df242325ab56bbf0fe3825096eea4ee87975b43e5a5871a3b704723fc3cfd3bbaee7668dafe77ecb7f91a31a3c1dad0b51110f38a9ebeb2433b34071c89d913fc86c6ea26f9fe157c0bcb4d8a9c55b70ced2114db067cefa60212f741203facb3029a17dcaf6cff505faa9c9f21f2e54a248e8c991f4f6e6b1209538e3aed38434b51ad8e617f01c5204cd15a15f9c091890669a499efe8db3071e710079946b49213d093fc20bd943b74b84cf5acb8e9b8665c01c2ea66f904828f84e921f5b18f9b8dd0cfe550e8d235d94e1e64897c3407e6035de528fe2a28e1398d8ff70f7e6279fd62170f773f1c0bf81c59f1e5f0b79199a3a5a386a4a6a4f76ad4cbb3aee2b04791d47bc67f9d5b8d5aff230981ef96f6781083918d0fa2ea23d0bec1198ca8139774b92fc684e970398988935723a5b875eb0a3f40b72fafdd108eafbe5a5376f01fabd18eaafb891936197564f73a0deabc5bb2af49f0b8abfaf1de66d5f088505f3b5cf68fc7a436851862fecd99f28ad14b346a0b498815854ffdf93a6a50751743a66305d6e91c2e21d75be470a74543b338c04f1c8b02049d944e74861d58b562e0fe8fe7ae64fa2b6077efe021a7ab556d69e204184a2c15e89482672ede1dc4d28c0064a8a7f9163ef025adedb06a37701a7ca4733d73ab5e8f3e246259aeca9853e1eb2b20454e88840e0dd05ca82f10905b5b719f233a9da4a6a48aa5a7a5f4b4376b73219ff1af5b80731293877e78286fd673f878000cbe9d7a162a2896dc770c642572bea575c4b36a687950c794484bad7dbc047a3a808c4a4be5d788894dba802f70f518429ff5fa110694c996978c5cc036814405d55d6fbba7a36c43e34550b373588ef0b7cf616148d749886c09dd4b9c17ff3120e859f1bb7b0a89107ddd3b2fd0e38b52e8ac7431874d9e7e4efb0deeefbc17a3cfa3f7bbcc4ea0cd992275834e226de073a2f974b2df0832d3fb1cb102c8228d36bfedd302ddf898ff4d9bc82b49357273c9fead69ba8a3e235d793fd5a99c5988877560a9a024b3fd1afb07a0ecc1bd6cdfb83f9adb3024113f7e82ac4e684b7bc9d13aeeab049ebd44a188a5a7a58ea6a8a6b43dc6b1373888e0c4eb2db0b6e9aae2de9ff2d3f142f29c179cd4aae2a8cc87848579ae95d60ca2218d1276e9134de338e5dd9f348e0a2d901bd37254ac045a94a379a97d9b5e8b42ad517e058f87267e1c37340950f978103876af35e619743485d51a74df1690a09f993dc718fd4fb71eb4e58a9824b009351429d92a763c2cf5e8b54cbb46ad5a9b7f5323fd9855e0121777381bb995031dc60ebbd765458d470ae12058c65c873bbf49444c8fba8348300b2694d2d2d51921e975198e6f48d909869825670bc35a6a8af21bd85d909badbf1c975765df40b7b202421f98b319fd8352d3142088484e74f2128a8ee5e53e4626db2ec10440fcb35143b8e9e94737bba588a6a8a692a7a9a78a0d2c88d0f6c8651d74116333bb97a25cb4c6fa10da663746f31b05f7870be7fae2bd30b9b1d6d2a696f07f3a1bb4a685843307fb7d9cf2bf5f4a2aa98dd1e6ab6cad647326866f665af001622ad5a44afd3ab4309861e824656516549f85e8ce81a732e82d80b0a9d6b13eda8f07e9858faf2c911adcf2d720c40336a39c7714e4eae11c6a14851aa8cd1aa57bd5445dbd3285fb149cc53c1a7e35951b257c971b1b1abfeb79cbf876b00dbf650f2a23965f6b3c955d36ae43967e19461906557c84c39e6237cc5dc0e9bd82fb739190b7185352aef31e9607e17e5742c5647fdb06a351fffed67d59d52cb26eac2f5bac1531e709df0949ea8712d579b9028fc93e53a988a7a9a796a8aaa8e4b2040342291c145c7d635946f44069d5469636cff75388b0a75e62b9a0a6163f84b6e209058b9f96193d1aafb57100ffdb8f7acb000911b4d6cc63616298fa83592eacb2811bcb20f7e7f8bc06b94b6efc82118daed3626e39f5f24e517fcd3b6f48ddd720ecd1c0f84dfd13ffae0da63662a10c332bcb13bccb62ff715c76966b2a746e2c2f9cb22b2a95600f20c1c456ed239f64bb6421980f10f9af59ada15e12d2126b68c192f59c8a124e62b7376fbcf4f7f4d9ff8ff2a90403c4156942db128cd06a1050226791a1b6fecec6b882a62bd592514be75871b3bc11b24d8f5a9b9dba9837c0709a7db182a29b76fd1606fde614a3c2c3af53b5fe2192e93cb09340ad88a8aaa89aaca9aba96af26621eca92babda2df3ebcd2fe269946b3bf208183569258630e64486831d32ca424b9c56ebb71dbfaa5d3bbdfa2ff62b397bb9cd0405fe8cd89d5ee774257aa55817f41c5143b789d565b46821e25478f4c2e751d5d3e8bdbb1ab590f80b5a32b83df6bc0f101171246fe477718c3284fd52f531c71fe74d43fec9fa3b7867ccfe77d4fc427436009e0f99f7aab4c3f3ff3a176ec3d73d41c2500733b51783a969b857afffd94cd894340cd8ada4b2e6d28f1817a8a8c4801e7a8290ed5838f32c4ca331e32b7e7c4bef052209c4bcaf2201124e37bbb1ebbb2609946e7c49cabadc4f68bfd9b90c7e5dc1741f4e05a2c59f9b57b1fb752095a3537da3a03ed57761d1b188a9aba99e4b0a0904000012331101005820d85b7dc2d6be69c5cc10f0d128595352354e57fbd923ac1ad3f734518610ca73b7df89331a010020a28f6a01a879956a02ac79996a03b0799d6a04b479a16a05b879a56a06bc79a96a07c079ad6a08c479b16a09c879b56a0acc79b96a0bd079bd6a0cd479c16a0df1c1f5c90fe05e12c8bbc9247364eb723329f25f6c5d7c9509f7df04b6437990eb9c82e8d879c56a0edf79cc6a0fe379d06a10e779d46a11eb79d86a12ef79dc6a13f379e06a14f779e46a15fb79e86a16ff79ec6a17837af06a18877af46a198b7af86a1a8b78b87d87dae0305b2e98eba18432d765bdc4ad6467bde6a5e73c421c71166391bdfb7f8f7afc6a1b967a836b1c9a7a876b1d9e7a8b6b1ea27a8f6b1fa67a936b20aa7a976b21ae7a9b6b22b27a9f6b23b67aa36b24ba7aa76b25be7aab6b26dfdc6f7758f01a67f5ec92da1bf7ba9c7a41daed3fd61c99e645bc268847d374f4a502e8c27aaf6b27c97ab66b28cd7aba6b29d17abe6b2ad57ac26b2bd97ac66b2c30cad510db6eb4a00279e78273a7288fba33d1cc2a6c4d3410b9ae20e3a0efc3051c646cdd7aca6b2de47ad16b2ee87ad56b2fec7ad96b30f07add6b31f47ae16b329828c2748b63703fffc2f212621c471eacfb9206bc9bee2226897e64f6b002403eaa58f3f87ae56b33ff7aec6b34837bf06b35877bf46b368e245a1a9029030635764e85a65bf643e49403d9cfd99695816eefbf5797c16ada6684b88b7bf86b37927bff6b38967b836c399a7b876c3a8aefb14068c23b1641ae67d52a535bda21a281e2ebbbb8df9ff7fe86d1f17240eb0257829e7b8b6c3ba57b926c3ca97b966c3dad7b9a6c3eb17b9e6c3fb57ba26c40b97ba66c41bd7baa6c42ec5f4938501328c0e310864e6e2bf31d82c88381ab27ae1575c699eb43b56741374a9dd0c17bae6c43fb53985710fc24e8acb1c7a1f6600316b0cdd08cc6cfee3241768ebd4dd736e5d4e6d302c97bb66c44d07bbd6c45d47bc16c46d87bc56c471ccea1c002317b95ba00df08d48bf4381133c4b003920fc37aebf9e3ccef1b8465cda73cdc7bc96c48e37bd06c49e77bd46c4aeb7bd86c4bef7bdc6c4cf37be06c4df77be46c4efd2cfe2173f61a7f7958e5b378bc3de9e86c8a503172a344cd0cf705dde777cb0fe67d33fb7be86c4f827cef6c50727e8571c9bb7dde071bfa14a063aa28bc6dca3496e5e6dca368cba203c56ed7c5ee8661867cf36c518d7cfa6c521254bbd63f24a83ffb668bf9c604331c64646429cd5e63b998e11edb9aab9a5b57e31a62917cfe6c53987c856d549c7c896d5583ee06c35b79440bd2608323b17c3b91f071a00799c1bac6f4fd34f0f8d83b333b082f02a07c8d6d56a77c946d57ab7c986d58064d2fab7700f63683cf5c88f694c945c83750c22dae78d37079ab14e8bbe601898db083af7c9c6d59b67ca36d5aba7ca76d5bc8a5afcc83323a168883bed7c21386998a199eb8ca9dff9ad3ebbb009bef69f1f2ca267fbe7cab6d5cc57cb26d5d24d5532adb1dd4c71f7df1ef4c9d9d36b9bfa3dcafa96a1fc927ded0e5f9d4c79b6182aac97cb66d5ed07cbd6d5f0e01b1cff044b1a57eaff08863dbb8cd6e33fdd68f33b7182c7161ed027986bd4618df43d47cc16d60db7cc86d616673405862cd1f38946447d72752682f38ace041903fd5244433fe234f8fb9bf172dc909df7ccc6d62e67cd36d63ea7cd76d648cf8f7344f015297309e91d6fb9b507ef30244b3351d1526b8bc53d94b6a71dac58a38d0ee7cdb6d65f57ce26d665e87bff0f8beb5af8a1bba72dfc64df12e15aac87af27d5ef373cae18cd506a7571305fdf97ce66d67807ded6d6816fde7e6f8a1a8cf3234278f6d507c7b5594cf3571dc17e00a098ffd5ca92521d30ec68c847df16d698b7df86d6aa7918512f171bd722d95a505a5e3a7f3217d24fed4af26852e4d3909c5b8a40f8bb8bb578f7dfc6d6b967d836e6c6eb72623a4b283d0baeaed61abe0f44fa871d3f6806642841a25d87996787ad145928ae19a7d876e6deb6b026c578b0a5b46bfa315475c494c7ead0984e3768d18c100da31c5e9f6cd5a3011d5a17d8e6e6ea87d956e6f18bd63717f1319f3407bae37de01199663017dcb5d935024a457d764bc504ec725fae602ac7d996e70b37da06e71eda1b1c6e4d4304475962ab28ce49897c35aa424a90cdef39fa83f03a3c61802d8b78818b77da46e72be7dab6e7309641058aa43f2569587de286b6999edf39d64198241ed21afb0a93aaaa4de2a9f72f608c27daf6e74b797fdee5191ff693058c4bc726727a3a11b1d7d4137511994228b3819e4032b59ad906ac97db66e75eb9ad87094e06f8cc3d30222432412c1452e2211b79cb43f0a06ab260da19bb2c9e83c76d07dbd6e76d77dc46e77db7dc86e789dfed414b46b5ef758f7c58b9b7e2b4a94cf57444118b2dba4341783eece06ec5f99378fdf7dcc6e79e67dd36e7a0d565a2d4aa3635a1dc3852991a4fe343899bdc27d3cfdbca4533b4f9b8ce3c1ea5298e3ea7dd76e7b4b63789cd00793df2866017c45d96d99e71a9fcbd5e4236f6f539009c9e45c3739399fa3f17dde6e7cf87de56e7d8aa72544f98753fb5e37cad6d230a9d7f2b31017f74b3f96448093d5b1fd4de472126c4efc7de96e7e837ef06e7f353b766bf50975dc39146141117332da69d7fb27b51056717438bf9a34f34307e6bc2b82877ef46e804dc3f043d7cf64810a34d4b2f3658201ef47a7f5250996b2db01085d8c4ce2acf6216c848e7efb6e81967e836f820cd59ae5d634b28aa9afcd2557618444f75edffe1413e0b6b8b36a104c56cf7fe42e22e59a7e876f832aae551a9b51cf3f80540807a8ee327d509241937052d8ea2c731c257962b596834f14f4a17e8e6f84a87e956f858c7328d7100877bcae64e02381afc2a08c854e2f47aed8820f2a3973dcd518027e62fc34ac7e996f86d5d0d170ec03118f4ccf8695d981986ced44c8161c898339951d2b9dabbd1587f00662feb37ea06f87d719bf010917cb4afe15e75f0f33d19318395ac519e803a0cd0b70d1a653bd17915f8067ba7ea76f88c17eae6f891ddaa2d2c730b72eecb4e7ccd18db54689ee8c2e7cadc86bd388eead42421fa1eb22aac6c57eb26f8acc7eb96f8bd07ebd6f8c04d15e560786f01d68f24ce2cf1b4ba4fe2608788485a1062f88fcf1e0fc02aca405c202d47ec16f8db99fa59554d421f263f43289005aaae2fea9a44980ec6b59fe5667e1fa4e3d3994923275db7ec86f8eb7405aa1bd87311edb4cc621ecccdc1635cfd27af853cb3661dafc886bc44130b8945dafe27ecf6f8fa07bbe64cb248cae306f9b062d85b1607d49113742c34f17ada91d618638ad893ba2cc6ce97ed66f90b98f96ed81882bcefbdf62e017cf267153e8f2504e6a3dccf38900902526756697969e66f07edd6f91ef1506c8f69127f1123be303879289613f4c857096003e411e5b8fd9e9a66dee9ecd4206f77ee46f929d1ebdabb2efc5353c6bd43240a87e98c70c52187388e168a03863c4c50113b30af76003fe7eeb6f93857ff26f94fe9240f8229ebea31811c73e887f0fa3beb17fe768000bc9ad036f02c23aa2e4b30e9bd2897ff66f95907ffd6f965de5852d321011ac8e6745225fcd76f84c5201c09eb07bf233d9cbcb5d519ccf8747858b947f8170979784898a85c9bfd9e9232bec799ddf39eae6399b4e67a9a7f86cfd179bf76751f7f870c89b7f8870985bb3538d1afca1834961eadcf1ab536b375097d69e490aedefec58f22bdd95187f5f4974a27f8f7099a97f96709a594ecfacd915baa073e4deb009b4588b5b0ef4c3fe9cc1fb572f34cd1427e1048921ec9aad7f9a709bbfd57caa61864a2f9223a36f938255ad361a5fefdb75bf1baaf1591385804f5a1e1eefe8b47fa1709cab5eed994ecfd08e09f5ee1ebbcc5490bb71b04de71542479e517d2b452e55ba66337effbb7fa8709d7d83a6b2cb39996955e8db22afedfc03e689e92a5aa6f846efafadb87e6af39ae28c8346c27faf709ec97fb6709f57dbb8034d6401929f410b8eea365748a8ac4c73dd4da83ce7aefb049a451fae75688964cd7fba70a0dc2a5a061f391be4c531da188173c0b7e0738168b64333367110894bc639baf29558a842d47fc170a1469285a93eac16aeba585922019de7aa36d08a092e69b969149d3b0c30aede2c29c47cd4db7fc870a207a8972b80025af851f7b4ccffbeff2d3378052ae31295c4512e221ba39e850b364598f9e27fcf70a3e97fd670a47e3c7c11cb659b06d28c2fa6a2a5852d23c7e43918830f3c5df14762c02a70bcf397dbffed7fda70a56482407f923a098e28f30d59e111f805529a4f0c14cee8eff815e5fb5d223d2402a39cbdf47fe170a6b9f0057a1972d9962749b2b7783316e92dc640fdcd16dcd87813e57bb9c1be091fcdacf8fc7fe9704ca772dddb734b7626510e8780b461f462a21b1cb9157423f84d8f31a80d4a573cc8fbdb2c4583a7f0a88aa8f7a991c424c05bd6bf3948e199966c74782afdb9bd1846092d343104357be1a427a2c53df2fa8ea9fbaa7e4675e0e1972485e29cca81eef12c8bc1a6df730f4e40bd3a34a5f8b5759fe885fefe2e95aa82714cab2418a2b9448077dcb9b21198878522bebabeca2951513b7c6f6a769c36008a5b5fd395319cab89acb73216069fa51b412791d4e634ee3abf00bfb09c74d148c4e7322b2c172cce6bd9c4388ba3ac90ad8988ced1d174dd0016c5c1f3f8806ff5ef0915fa3abe81d209b2a40f1b8b90dd87c87c93aaad97ae86dfb6bfee2ea0b2464912e78c925fed820e19c363e4ac4104f0bc7e4385f7d8980f670cb1ae9eafb8afa5b0f1dee8a752c54ea90802e9bfd220d878695ea26b574527d264c27a647a82529b47f32e73bcb0a9b1c3b1b0b208092231caeb7c1d6b6be86fdb6f163c99dcec091715584f3f3b82f57f1f58663e1cc780b2b4b33e1b66e8b9a7d490bad66d983ef55361ac5067844f98c39e525c7fa4c5c04c157b52cdb1cfb3bcb4d0b592aebc130af3edf71d7f3125f94550bb54ee7171813ef60d017385a6f643bfea02c1d6b4c3b58af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e803e3a4807ddb5cab6e4b6d1b7e8b7d5b8ecb8d9b9f0b9ddbaf4bae1bbf8bbe5bcfcbce9bd8081bdedbe84bef1bf88bff5c08cc0f9c190c1fdc294c28172c398c385c49cc489c5a0c58dc6a4c691c7a8c795c8acc899c9b0c99dcab4caa1cbb8cba5ccbccca9cdc0cdadcec4ceb1cfc8cfb5d0ccd0b9d1d0d1bdd2d4d2c1d3d8d3c5d4dcd4c9d5e0d5cdd6e4d6d1d7e8d7d5d8ecd8d9d9f0d9dddaf4dae1dbf8dbe5dcfcdce9dd8082ddedde84def1df88dff5e08ce0f9e190e1fd723201005002851aa8b33107050604000882f0010de101e31d8e0c80120dba60134005e0200028008202600008f0f2981002010000a1b0000200082082888021e0047934c6b51983e9c83cd7660d8505f7e81ecd7b6cd6413a32cfb55943e1c13d6bd618a443f71a9b75908ecc636dd6507870cfda3a06e9e0bec6660da623f3589b35141edcb3b68e413ab8adb15983e9c83ceb660d8543f7acad71900e6e6b6cd660ba92ffbd88fb3b739eb55987c2c13dd7d61820efeb061cbb776cada170709fb53506d3c1bdc6cc1aa43b91c7ec68249f8525a39ee1d688c03625003de64447005b940cf40c098ca8e1cf6af6324e756255a0f00c2b8ca6d7d215be218043397f5023808d345057c68fef19cc8cc4f3b205d463be3522b84dc940cf90e808408b12801e73022342f8a6c302fcf58490118cd4b1b00469ceffd688e03605c625f7e37b067323d13fec988c6617e6869bfc403fc08ab3304e3173a1870a12c56461cf691807ec19e845c50a1b8e403c168573e66a74d083c01647119e3023d19c18faa95463c12d80a79859c8cd053dd42031260bf71cc338a09e402f6a54d87024c421bb02bb534961c32310c744e15a349e32abd19c3a7ae944609d63084f9891684e0cfd54aab1e016c06b11384e59851e6a901893857b8e611c538d53760d74a792c2351fd8700ac4b128bc168d53ca6a74d087c01647235cbbc00965249a49433f95348ee9c696db008e336ba19b0bf25041629c2cec3916e358354ea92bd04c2afaa9a1714d06c6e4c49e5318af45e0386515ba79410f15248ec9c29ec3308e55e3b5289c3357a3993a7613433f35698ca98d3559d8f318c6b1d6386557203351f45387c69a0c1c9389353db1e613eb64614d6c6c3986f15835aea5c2b5689c5256a3833e04d664e0984cace989359f58270b6b6263cd1716bc8e712d15ae45e39a2b1cb07ba39b18ba13473737eca0b3c63a5958131b6bbe304e06d67ce28a2310c744e15a345ebbc2b114b8768923ce483493866ee2c8cd0dcda4a09b2b9a13433717e4e6826e5ed0cd05ddb9a0870a25c664619d6cacb985311958e71363b270cd07d6dcc09a0facf381351fb8e6031b4e81381685d7a271cd158e45e0b54b1c4b856b17b8e602d72ef0da05aebdc0b5b7227142b773f93438f7ff13493094d36719ed1abce27e10a039bbd8db972b9f8b81819e4200d0828ab9ec8e026f20ef315d7e2ed5b3f38238728ed1ae825f060ae805ae518808d5e34a802426ac7a7116778e41b2a4a862f3ca9c1d03bd5d73918d3cf4092c87a95721971cccce6cb817f694738cc38fd871310ad041717e405aeab94818a82a90704f159094bd07f2be5385174dc839460099a2e6b15487256720f0a87295a4475f02ab0186ab1e9736e0c52cb817ea94738c931f98210f040aee027a197aa425f0190437df5091e3f17101454e48557b418a38c7a07949f32058d0115c8310bcdf8b964dc30aa64007070dcf9a9492758771089c635ccec08e4301669c9728ca7bf463605520a1be688482c7c70514392151ed0529e41c83e625cddb50c09ad99e0b3a81d500796ce399c5e3c70514b990b2f6e22ce21c83e693a68360c163861724e1d187c0ea8081f6e3d212a09e94f64215e81c83cc925a415f50a0c46ce10c1ef5089447898c93e70a89904b07786968c80b7bca39c6e167f4e618c4b7fabb7c402cf0736132505ca648ad05fdfab479c8a51f587100ecc53ce11ce3e20eec207eb0eaf42d50d2a35f02ab41c055c8d5065c3115ee0539e51ce3f00666c881c0da5d85831e7aa425f033088e6ff86e3c422ea0c809a96a2f4811e718342f6945f85e3e97d09501d24be075e01dca339428122157bde08ba9b117ea8c738c9b3f30031a0b8a9bb817348295e2852be46a0fa63414f6829c738e71380366842860ed5e2ef8a4475a828f02a6edc72577a8992adf0b85ce39c6c90d98114601bfc0b5c5173e2a845c1dc0fe1740bc984ace3114f426bd031f6cb6253dfa12582d8ea4a8fd58409d875c01421613dd5ea8029d639059d2b82708d007ee50f0fcb179c8a513f86202ecc53ccb8518f3f56fe0880c15ef7f0f497c72e148e075e01ddd63456478c0fcae92ee222b3dcba8d11725783e88022e39d0482b563041d0b302feb64051748e71b881ba608331263cffff803f94287ec0e788657d5c39411613d05ecc029d631059d2b807045899c70aa2f94a7c0cac0a29c07e5c5a00e849a14a94179fc28b792b7614753d4972f0e1807eeba394c0eb00e39d5ba0bec95b614fb282f90289fe9cd215f5bbe99799f5f227ecc068ce3fcbe88be015cf18015a3447df06ffde0b9381b2800b1572c980d10f10b5683eef1ce3e10646640381efae1776e89196c06778bd0ab9f2201352d5bd20c5b210436e0629f6d57de17fde16ccf6004c8601e294e28173e398e38573e49ce48973e5a0e58d73e6a4e69173e7a8e795e8ace899e9b0e99deab4eaa1ebb8eba5ecbceca9edc0edadeec4eeb1efc8efb5f0ccf0b9f1d0f1bdf2d4f2c1f3d8f3c5f4dcf4c9f5e0f5cdf6e4f6d1f7e8f7d5f8ecf8d9f9f0f9ddfaf4fae1fbf8fbe5fcfcfce9fd8083fdedfe8483fef1ff8883fff501008c83f901019083fd029402817403980385049c048905a0058d06a4069107a8079508ac089909b0099d0ab40aa10bb80ba50cbc0ca90dc00dad0ec40eb10fc80fb510cc10b911d011bd12d412c113d813c514dc14c915e015cd16e416d117e817d518ec18d919f019dd1af41ae11bf81be51cfc1ce91d80841ded1e841ef11f881ff5208c20f9219021fd229422817523982385249c248925a0258d26a4269127a8279528ac289929b0299d2ab42aa12bb82ba52cbc2ca92dc02dad2ec42eb12fc82fb530cc30b931d031bd32d432c133d833c534dc34c935e035cd36e436d137e837d538ec38d939f039dd3af43ae13bf83be53cfc3ce93d80853ded3e843ef13f883ff5408c40f9419041fd429442817643984385449c448945a0458d46a4469147a8479548ac489949b0499d4ab44aa14bb84ba54cbc4ca94dc04dad4ec44eb14fc84fb550cc50b951d051bd52d452c153d853c554dc54c955e055cd56e456d157e857d558ec58d959f059dd5af45ae15bf85be55cfc5ce95d80865ded5e845ef15f885ff5608c60f9619061fd629462817763986385649c648965a0658d66a4669167a8679568ac689969b0699d6ab46aa16bb86ba56cbc6ca96dc06dad6ec46eb16fc86fb570cc70b971d071bd72d472c173d873c574dc74c975e075cd76e476d177e877d578ec78d979f079dd7af47ae17bf87be57cfc7ce97d80877ded7e847ef17f887ff5808c80f9819081fd829482817883988385849c848985a0858d86a4869187a8879588ac889989b0899d8ab48aa18bb88ba58cbc8ca98dc08dad8ec48eb18fc88fb590cc90b991d091bd92d492c193d893c594dc94c995e095cd96e496d197e897d598ec98d999f099dd9af49ae19bf89be59cfc9ce99d80889ded9e849ef19f889ff5a08ca0f9a190a1fda294a28179a398a385a49ca489a5a0a58da6a4a691a7a8a795a8aca899a9b0a99d80040a110500000d74783882beb139d912df0328b52ffd6707814a29e7086d0e004253453960519b005fccd4add86a7be3fde386f149f94de4995464d4626b4b5658acb58cdc9ac0b55e883ab78ab4774a29a5e4465fdab6ec9128314028039e6b7126120a297818405111002d6894684008012620421986840200463a021a40483e142e222818b3b8aa6eb86a960d81e75a4cd938c819933326f7bf7ffea27ceffde534399def25844f1d3e9fd3217def9f8c4efde17c2e5dfa8fefdd25e8f3dd5f8a52ce7fef51a2d214ff5d42ea3ea3dc147e530e532c2e55d3ba5ba9ac2b83934539b743392797d74ef65618a747eac9b65dcddada54df4dc5fdb950bdd9eb5a998ebaa9b6567fa8be9cab763a6a0edf8e44537db38eeb8f6680612b7675b54263afb593ee5638ccc1aa6ae2c0815d19ee669dc57a74045328a042281bb20111040400010300230882230c082d9b3b374d00b5c5ffefe1a191f3362d557340027b9a08e6027e32216a62b08422098e983b93d5898b24fa3cc492a0a683a20718854e38d3cd42cbae7cfe8a8e13b606743c6a634d181309820be410860005cd37df89c2f0e4ddd0088e6166778a320ffc2c28f00016887e3c1f338662c14b2ec67e19861763188f7816ccd64b0ba6107e046b58a81be82a642bdb73078dc98e58457f176e1e4068dd082e6806900a33c51a010020dcc96ac402917472711d0c101912a302470007814a298f00850800940e81a56454797065782c412e376536306466303432613963303836382e546f6b656e7357697468647261776e675901017b2274797065223a224576656e74222c2276616c7565223a7b226964223a22222c226669656c6473223a5b7b226e616d65223a22616d6f75554669783634223130302e30227d7d2c66726f6d4f7074696f6e616c4164647265737322307838633533303365616132363230326436227d7d7d5d7d7d0a6a496e646578026d5472616e73616374696f6e49445820a9c9ab28ea76b7dbfd1f2666f74348e4188d67cf68248df6634cee3f06adf7b1706e646578000d003795217cd35c4fd8f824a6bd12966e628442f05ce8d8354706da421e591666e3c0210f2f095f1d1a010020dcc1c7e0034cba31e30812a08c006d0800644465706f736974656458ff746f37353461656439646536313937363431037a96bd12926e6c185c821fccc180ff954ded0e8201ab12df0107750600840b73666c6f772e4163636f756e74437265618c410006003795e107d38e1e241b668001734efc449901239e82a70296041af5f8081d0f00a213463850619b0043c9c7ca4535479443589c5a8d88964cb48a9048ec6dc91202443c7b4810434c3ab291356426493b253791228c27d6d5ce1c7a0403a0b7571329f00192adbc4309802c4001ca429412a192e0a160723240c0030281109206060b89cbcdaeceaadcad5ba643007a7b2f1a07dfb7f9be0d9cdd91cae812baa4e6844e7b6a97a0ec69764b529cbca1fba41a5249b66cda3d45efa654763b9d74eaa967a4ddeede9172a91dba943d41196737157d3ca3f3eceea54253b7cd7b8c7561181e2dfa3e96dca26c73a8af737e430992d8c271bf595bb9e2efd4cb214dc59cc5fee270c45cb1bd3854f17bb0fae188bbfb9f09aef835afe35087c185ad19e6558a0cfbd65abcc774d7935996056223c338dd6b5ec68e70045420c042a0137500371743bf78e3f54588bf1b82d500980364828135408e1b81de6c854541e47dcb0c5d22c88d0311e343bc6058251b0173e031cc8c0c4e646aac11d444044a146f187c1145cccecdaec8ddb18ad63732213c322802188c1933a3ccc088c16146466efc79234013c060ca6e0cc32d38da318a3c50632783f3453c83dde8cd5f70dbff460688c1871b9fdd0c8c0b8ae298b1410430f1deecbbe56e82b7f330ee4c0b7d0b31668466b0b80cdf2a1c85ed0d074d65076aa28f843b0310e0dc0868610606ba99e71a01002093806b1a23a69cee0d29eaedb9b81d8b12ce3bcb2c1ec902e26fd6ab2765d46f9593efa12ef45042937ac4c136333165383861653766316437633230a32aeedbc18074f4758180860426e5eb089d0e007213463960619a009f65c4ded9fd4de5df5e71ec57360ee41341914624b6b64c55875166243a42008da810518b96933253ca94bc18e131b080e6b66b6d039d6bf1e671e15820f0015221228021a440658d7c142e272503b00c8483001f13000d1609130f14ac619e553b5c398b86a0732da66818e49cc93993fbe343389d3af588ba34a7f91f1d4e1ef943faee13f5192728ffa3c7f833821fdfdda94bf2237f494e11ba14bd74392309e14f3e692449ea3ea1dc547e530f53243095e3be5bb1b0ec62b34139f7755973de2bc5e9719a937660575d77396b6b537d3515b707437567b38b5d0d75536db1f6507d3759e534d41ebe1c88a6fa6e9fd71e4e0185ad5af6d5080c7badadbe5bf1f046b3acca6143cb2eef6e9fc59274045328a042289bea0011040400010300230882230c0841563d374d00c5c5ff8f38be4c8540016caa50374312e6fa60640bc77a90a52b635b9544d6cbeba1d578c28c50c440250a1545ed001d7ee009faa40af0de44020ea330a762bd67f564df1a2bec8c75b6fa68e8fe0f5384b3680203301c8e02065a361b6322ac113e1f7f98b0fd384be3d0bbd98eb80a94cf17f0326be237808e376e45c46098702e9868427d4357215bd99e3b684c76c42afabb70f30042eb46704133d35402b81a010020c6b36b26602dd6b7fad80b0e6869eaafd55625faa16341f09027dc925a8e8cef267e56838c767c13c67ac4c13837373933313733366565373763666661196a76c1800bca066580c22ca18c0075080074302e303100d5c18baf6c8d11f0693e71dbb951c4856d4f25a456f4d5285a75fd73af39161c7a4284a2f1415788a7c0e17ac2ce6bc09f8a0065080054fd017886bd128e6e4c4361d8d6c0820432e1e8087d0e0022d4463860519b008b5db12d453cdec6f8350b00acb8146a261119b5d8da92254a34922977a372ac47da825bd0db32934c2979115f342b5642044807029d6b71a6110a6d501c002a3e161820830ce50e18c881424643c705a2500458103000210212c198c55575c3559b68063ad7e248a620e74ace95dc0fff41f993a4f2e5e4547ef487544a4ffaf409e1a3f3a34309fdfd8cef3efda7e8923b9d2e9dbe87324e3e3d9fef3fcee8d2f4f12597a2cb87ef508ad47d46b969fca61c8e4050aaa6755752599765934539777cf1bfba57cf466a8953e7f563dbae666d6daaafa6e2f650a8deec75ad4b47dd545bab3d54dfcd55391d35872f87a1a9be59c7b5672390b015bbbafa80b1d7da4777251cde6055f54898c0ae0b77b36e622f3e015328c042a09adb49750f375700b9c5ffbfc0415e4bf8c1aadd3256980616000a57c84220e13d1e42ba8f8109a53a40c70dec5aa80b2a2dca18c4319481218c9af79b37d689a1c86c03f0ba62398d3263db80db90ede08867cc46c4007e6f126338d26906ab06cedae71a74475410d4040eb681c5eec7d730e345333adb82779f6e9edc4968af9823348745dd7057615dd99e3b284c76f22afabb70f10022d68de0ca66734058241a010020fc7ae96b32fef63df18a5b986ced4abc3c1944674d313a6a1798ae9b8193f0dc3ad6cbcbc20d91097cc1a0393462383464306331316132323430349f3b2ea93280f29fd83180c23640dc5b6476698dcc8d9804fe29873c3e767cde164060761c871674d7f967b97951fb563e8f7b36fc6bb2918a00f50700528e3533706b13e1f4134748c0050001f6a83a3f8b14dd24094f99929546e4df9ca1981672019b90109328768d44cadb3b0ac4e888d60100dbbba41c46a643c34238045ac404c241a12924700c3e042c3a199da83966635270a958e48cd6ea2a5b536501dbbbb4480cdc0b71efe517b1268faee99ba23e3a90b6c7b5bec69d82b59cd1d286699f7b397ad06389b4458687cdcc9a217fd6923b0ce4ead8c78b1f66c81eb983fc1bac357ed8cad71618f253ce72e771f19e18d70e197f1beba45fa9f22766055112f52c848e48dee54ffa09a23cdb53aef28b8a786b1964c5b2c23a920dd9c56a4584dfa258407f1720d0fce5248f9ae0e1047160fb183669f0831b72ad48cfa23a8f6cc09f946b5f3a3a930442f2ea08050f00d293463a6067db00a0200025c156828ddb198242793888ab62b0a6b67b79496e201dcaa4e1d62ea25318b646da08409b9292e49791dbe657d41c71969a0d8eccb5d822d0d834303091192e36f5c0020816058d040e0c1e2c3c3c14c101020a008aab01230282ab298b2a86eb25b93832d762884521674ace94dcffde6774477dba742a9ffba4ee699cf2f9a71e7dc27797eefec9f9f0797c37fd1f870f52f99eba740fba3ba431bee9eff18f8fbbf88f7b09a9fb747253f84d330cb198d4ebda6e249aa88a66393977c5c2a7d139b75092ac9b19d70f0cbb97b5b5a1be978a5b33811ab3d49da476baa1b6536ba0be1a5531ed74862f46a2a1bed696b5464bc8b0b5a2b65a11c05e6b1fdb8d6458aba2e80121a0a2a4ec5a9bc41618105120a04228ab59370d437071fbbfdf615f8af7843b3add409819ec18f83600669cb01bc375a3deec0da2ec44046066f37d3c2d868a6e50ee7922228c22f16021c656ee0b833d44ce573425892cc67d7937b9780b2369c0196ecc7137d6e9c6de378ec15f8a41067763cc008339de780c226813dfb8af1b8341ac62981be1d204ef1b310c77cb79b7314d9328be0f582416d3b8db07c685af64df4c102fc421ca0c60cc84df579c19e10c162fd3adc251d8de70d0a8ec084df49170cb00849c1b010dcd988a8b191a010020c67bb36c8e03ed1a02d50a00e41182724e5e5db3d6fb3a32270ce2d223b600c06af7c4ae619c57127d47a8d59c00950158ff3100cefdf24de288f148d286574e34e78b254db064ac9b41e62435283f2824ba994b00150000f303e402c902cc73dc223ce426f0350d277249faa632846f9aeb091b9fc4b457c2d24d8c50089e0b1dbbe6c8405bc823cbc26c1c38e40102473a2185e415028d0a0094ff3730646666346431303035383234646203fd310114497d40c2b834380075e5b8247d5319a237cdf5848d4f62d92b21e9c6462804cf858e5d7364a02de4816161360e1cf2cf3a727f8542ff74d41dc243a10aa3671986d5f8e06bf96a47867e0d63a9eea268678f27e3879e1af1a808cd46650d5b74cd7b43ba6cc09f28b12f0643c247ccaea1ba99ced9e6e9214573831259e52bc328362867be7c398c764cc45d51ef66246cbae047cd6cc02c222974e0474eed6293443770c14d1cd67f7fc1dffb677f8d0001a03ccad522824d62674b1690442d53d23d46e510349324acb13349da29798998417f5abd63574414041dbe39db10088061a0c0858163006423451167e34042a3f2980e7410008e05019402034464ca98c55576cb569b5c0a3a7cf34491f07d95efabc0081dfafff8109c5e218ff3dda94708ffe7f3cfff4f1927ac2f5d4e4aebbbf418fd1dca585d42fe2effbffa538f0ffca7fe55fe94d41d72bea6f29b72797aa0584df3b0a4b22e0a9339dfd796c25f3db53ef64ad1393bd8d2b661ed5edb51e38bcdada144bddd0b5b9676baa3be566ba2c698cb66da692ee34c43478d338f6b4d8710712d7679d661e2e27b250f4bb88cc1aa4a7a54609785c39937b919160154289042b0239b0011340400010300230882230c0841b37537cd85733160a88a6f20e14c10d805541174268c77e23047839868772f1837b881102f58862d7a8b61631a59c1480077be558a1f0bd6842a2c1856041a2687094122e62640cc029396a2316104f3c85c341cc0111d8f12e2b50eb8844070d9b7a4c30006786102dd89cc3850112e4cf80df343cc01e4f663bebb1c312f34200bc8047c051be13116ee265e85cc62fbeea09bec404bf479e1fe0002cc8da0156601c90ab11a010020ff7b4eedec6c4e1c62c61d26c04278f8492e5daf5a08be2c443a41cf42d1260284d9ed8dc4d7cfb2d0fc32c1a036363862393165323939356332656261419654ae4e8069c7a7a480c250a9fc9f58665124235a4636316edb5bc605444f986374369ac1c852acec53d024dad36e7a8a7c50f76cb291804b13a6810586dd99ac380c204660295c1e4212659929c58926ff6e4db60e6ada041b1566cf96920812295f440e2401906d0100dbfbd4221d0c1f190b1c1a15d05a1497496003e3312158712888285c950508064135592695d6ea3a3b436501dbfbb4440cdcfb70efe527b12287aee85b921e3280dbe35a3fe34ec17a5269b50bb5cebd0cbde7f1046eb2d2a0a15973e4cf7a7287895c1d7b79b1c31cd92577915f83f5861db6f337081cf935ce72175279538ceb04087f2b2bd57eaaf32528f548507ad6414b26eff2073f7a28cef61a47f9c5440e98782d50c252e58ee839b415f8114d28faecd02c0d05958eeaede651dbaf1d319728f613378874a85d9552826dc09f6ef40d3f52c2557e643bd09c8ec65efbb1de2a53bb96493992ccc73d31bd2b512686f5272264be7167d583a47cc2916dc0888301c3a455c258849c2daa1637921607bf645616ef83d4ea052c24147181eba985f858ab6806cddad3ca53b358a06dc09ab3effeb35881045be0e708750e0032d34335606d132108c051618290a84e98871d26264b108a9048ac6dc90a356a19d91bc6ba000de17206c84c32a5947e8f54a0574570b9128d00a16b71b671728d70857e2420680c942c60292306201c1e1810260702240e231491908a040306631657d50d574de309085d8b311605397372e6e4fef7cf237c9fe8fb69717ff93c7e7c8f91fa8491c6e91fa7fffb3f94fffcdf21a4d22d70fa8f8f42874edd5fbef7ee8f4e124ef749ca3de1f7e43026625335adbb99caba283e99947347795fd6393b6f7e757f862c7dd9b6ab595b9beaeba9b8451baa377b5dabd35237d5d66a11d5f77355504bcde10b6a68aa6fd6712dd208266cc5aeae3e54d86beda5bb190e7fb0aaba285c605787bb59a7b11d1901532870422847650345743d374300b5c5ff47433002d7537409a62468f393a183c27b3c20b0705bbb08023e6372364e90551ed465584e1f3254f9591b4bc4ed271170bad963e6e2c54161a0705d73f5c654405168c347b73007612162a796ff0d7602890f15801114bc6060c1f157314acd71d292294c9057b69b274d1a17d9419f64bc6b3e69ba6f3d6bd8f871a78116b21061840c58a41b7715d695edb98364b24357d1df85d303085a37824b98012bd026ad1a010020c2af6d5bc9cafeb247d30424da537ab39fbb0ecbff6f3cd60590262ab839206eb26ce791b5abc750c27cc4c1a03832656332383366383861363265363596bef3835b80a47a91d580c25df0f9672e3c371b5fb9e137537bd6c427c2d2ad1cae13b746ea7909398ac87bac2d18949acd5dba6dc09f68eeebbb7c5dc25f65fca0c93c05207a5248a47157fef86fb7864d6eb86c00c9d4aefe90c7820106b3ad32f4d85fc56dc055745735d85fc56d32010050020ac28526a803b3cd82041200009989000318131d0e0025ab8e00130082e040a0280c100b00000030a052430010040002000c020104000310600068012836dea855297986d362114237c801e5fe5e3591b9f33ff8486415c88d39b07e67216564355166ac44a1e41b29a966cf1a04cf226b074002e9809446ca691237460912471e89b204cd5104aefb99f8825eac1b30a5294159d606cd9d95583f0a7bbf25d312627cf71bc93bf8166d9bff8854311070147a838487a3b518a7ac2c3d2bd5fc48985a24d21046899a5da8cf021ff7e212380970947f60ebaa83898b4bddfe8365fb7f53cd751d969228351a8c22f68a6e28702c8a17b11399b363c047da5791080305ebcb2c01a993d54459b012e1126fa4a86a4c0034cd22e5403be8a6bbaa269a6c85472e487b205b4945c0090e3dd2f36aa269668982dd6014b127685d341d203ccd22b90cb62eb99adf7b568cc4de475257342610bd483c24afa0cfd53ddc40da04f08f03fd48817da0305a547178b7ae4825de1821010f3e52ba4a6a6ccc81b5975a231d71ab1a98de94205ad60686cf4aac378a78bf25d31862acf75b55f5f52d4a36ff11a9c62ce008f4060a0f476b312865a5755994664782d622a5863c4a496d0a95c181859be6cd94324b266d80322ab1ebf597741ff5f970411d4af460bf514f89cc0c4c8b45912f88ca471f5313f1c8e0f18f945689989103abd66ca495ac26cacf9848977f2395728077517b46c96d8c102184ae173d53420d4f816d8820884700a4513bfe995700e7ab186af80ada0114b00406c61bd8d04ed18454f5765c263d93bb6fb4432bf79c5836708bbba389e1f8900020097b58e7da579132f9fbb25f815f7816f9eb4cbd0940c97a31709d81b592a7a121a551015035b87187811f0db8de08e02cea9e29837e12419225ad788f90013ca25a6a1d84920837fe462ad54b9e45568de03616211590d75e394a5a2a57ff10be30df3b1f495dd540000a903348a5521cab7b3803b509301d38e7cf7d5abf46b302fa1183d39b042bca7c63874ce8f591d255442ba308acbdd09b44c02a079c4f2588c9da4060b592ecbd22bb6fe1318618ebfdb6791fdf2262f31f912a06028e426f90f070b416e32a2bb586d5687a1fbe164937e4515aea53a80c0e14ee9d97776548da620280e084b7572102ff870a8275004aa2dd78c5886ef7b9b74958b4868f9cefcf2b5ab67b1b3566436a6f93eb38b2be1717a40eb2cdd28b5aff1d76dd5fb0e761a61669a5cf6c341f376cd3695573b39def2488b66b03e31325961829de6ec964642ec4fdeda2569f45c986e5a83bda11f7d2541b8e906324a5aa3d468f91924d18a51ef45d530b08dccc554f3f30c89fc17951171e3ef58e8ada3a0c2501695e1b78fb2ab1c0487abb45263173a9ee6f27f5f82c22362c475dd38eb897866d38821c832a550d32738c446cc22875d0ed9a5a40e05eae82f8210139e0600e5f17aff8a67e53255b87a12420cd6b03d726f5f1810dad52e762ee99437540cf43e1b95ca4beea1943b588b2b11cbdd315a44a2545280b383afa492a48e57369bf7a71d84ba4fbc1a3847429911b23aa6e9fc371557cb0f5ce712eff6f6057575df9ed6fcf05538bac9c6862ae0523b34ca12d1834c58473777ead78e8ef2b4c306689ad89466e591834a989e376f6b8f0e8dbd78b66a65a3161ca961983a69a70eece8f150ffd7dba68cc521b13ccdcb231685213c7edec71e1d173d1aa6f5f5698b662d58c21bbacda07334e9c9bf7c9c1436fbb5d30b7c4c61923f32c1836c5d4325bfe1c78f5eddb0ad356ac9831649785c1669c3a33e7978397de76bb606e898d334666acbc3ab2c484515b2c8d9a72e6a8ccf2c5c1637fbb2e985a64d50953732c189b6466b1cd7e1c7af2e76f85594b969a306a8b855153ce1c9bed8ba3477ef65d30b7c8aa13a6e6583036c9d4da8a41534c3877e7d78a87febec2046396d89a68e4968541939a386e678f0b8fbe7dbd6866aa1513a66c993168aa09e7eefc58f1d0dfa78bc62cb531c1cc2d1b83263571dcce1e171e3d17adfaf66585692b56cd18b2cbaa7d30e3c4b9799f1c3cf4b6db05734b6c9c3132cf8261534c2db3e5cf8157dfbead306dc58a19437659186cc6a933737e3978e96db70be696d8386364c6caab15c2e35f06a2a18cec0f83cf3503a2a1af3dbc2bfb61169c38b8f6fe3af8e8edd78566aeda1861ce960d43a39a586ecb8f158ff67dbb68caaa1913c66cb23168aa11e7ed7c71b1d1dfae0bcd4c193adaf274e285e1c08bc492178b3914e20ca657179a0c820f850a3217be012cf8002c7701026100b8322cd9ed9010aeba9c6448c5bf36029214b190ac5016e0c3f293deb5bf02f97a1fdac34ee37c61d06dc0d81ebbc2e361b46493050800720e3632706b1356910ba2c855ffc0a10204224b094eb49b2cdf99149b34b2ff96e28d34ef3385a9d86adb2eb76392bb3fc2fdcc611290ed9d62d206ed500402190c4e9c1d0b284e2ac080c252f1c0e3e2a2d191c1318041b0d02ab5aa2ded2c5e90ed9d5a2200ee7db8f7b2655510fe47121891502094b6c7d67ee65e994dabd4b085ab66d6732f471ef478126d52a14153afe689ff6aca9dc3c4d6e74b8b1fe6895d7237f16b666ff8618dfe2681277e8cd3b8fbc078d7c93543864e74e6109abc58fa1235425224bd8ad2059367f98b1e4295677b8c8bcea9087af80a2909f2b46493df6db190ed0700524e3532706b13fc2ff4054ad5c1e9f6a5ab9ff312571b49e2329362a710f2df72b80324780d9d5066b1dc3d4f4a79f9779d8ffd160500d9fbc4268eb9a90c082c191a2837170f94d5004245ad60c207c6c38687464701453299345aabebec0bbc02b2f7399910b837e2decbcfbd286a08b29cbf49e24132c7b5fec53d227bd26831066a9d7b19728fe3276571c10143af668affeac91b04c5d5e5c70a3bcc147be48de2c7c8ceb0c3767e4681297e4cb3bc7d50bca9d4bac1e26f63a5d84bea7c0a0a9f499a9c05e9e8b98a3be90f9e88933da641d758044109e394f2b16490c266a1d88a6e7207e7c322d0e5188fb0326a1ba741a1372213bfb3e190de856de3ea6288c5459afd7c66ea6dc09f1b50d57cfd66900468eff608ed0e000214463760519b005b11947b042a4ca5825c4788a8b6d675221015749664cadda82c2741841d02de99644a29b9af560ba36a29922314019f6b71a67130b0a900d011b22041f1014247a2001822191e979748fea2b2212d380c2e09148c595c55375c350b47e0732da64014df3f7f3f39852e1f7c29b9a4d4a5fbbffb7b9c527ae8333ee9e7732a5d467f4fffdffbe990fa3f8cf34d3efdffa73ff49f93e4509274f2e8124ef729a50195caba349d2ccab91f9522b5316aa657bd5af9917ab26d57b3b636d59753718330546ff6bad6a6a36eaaadd520aa6fe7aa9e8e9ac3d7a3d054dface31a142270b015bbbacaf060dcc1aa6a8210815d1b4e84025220b042282b6e00370d437c71fbbfef40e81bcd3058c686ab09983166bef785c11b1646086bf091bc59a0c6c20c33389088f6893cd3b7b993269c7dc3770d54917881192514211001e65ec45626223412c3b61b1783bc29b29bee9671633c5f63cbcc736f60e1063f9b00dc4066c04c5022a8591caa709f969b85f18c44cb302db5decdb3db69df6dc689713bdecd93289cef0312830c76e3dd38f81e406ee42146e07d059911c860c132da2a6485ed0d0781caced1441f09870c40ccb911d0d90cde96ce887df56d68e0b836de4bbe322f2f3258e8d3f7c5fe2cf839f3eca68947f5e7fcb0660423db6c2b3b6fc1a036646131613337623535643935303933c24dd50a688085876aa580c26a071e5810f1c8c934aec260f7847400af8f77607ed27ecc02668d7bb2c287c6838aff1f38937d6a806ec09f5b2a0ed76ac26c96c7382122436bd6f2f64451678f751992260dab0cc585948c0143b7a08659ceaf6502ae9e7dc28b6ec09404f2f29e6cc26dff0db565c74ac538f0c03f1f8ad2508cd816f375231b2c02151a090c8505590065223ef0a57dc2926ec099c4a272a56dc26f9bd0136bbef3d8acb3344eaec795a33a9d94a4bf16dae0c709404888d279de7e04c98fb0b06f9d6ec0b660308cb06fc2716d8baf21faeafb231fb1f4b514f3f16edaa51b8fb8232b03c60391c38819f1c4622e3c59bb71a86ec090c29269bb71c273dfde66b4e75baabec53698ee10b404951ecd7d862050861db990ffea3b059b2b91c770eac673b36ec0a82b3ccbc67374ea0254473a60559b00406708ecc6dd00804b75c4d62b54544fcf6422a395cb5bb282d4928cec3ad1ddd3adfd6805b137ed9452fa33640a043cb6834d805a14059e6b31863970798048085190487494012000443c3e220209161a10087028200960618082e18252c157b55155c335bb700c3cd7e2ca0642ce9a9c35b91fbe3f97d3a5e931fa94fff0df4b13467f193975f9efffa934fd5f2449fea6fb8bf061f407a5c39f34caf77f2aa347128cdee1740afda53ff519a97b04e5a6f19b6eb882c9d42cebae85aaac0c0e83726e2a3e093fc659ab4f4aa29ed7134dbb99b5b5a7be9c8afb93995ab3d6ad360d754f6dabfe4c7d39ab761aea0ddfce444f7db16eeb0f778061eb6575b542c35e6b27ddb56c98bb286a12c171599bed62ddc58e74045120a042282b7200370d437071fbbfefe84bc40bc53b1ee3068cc88d71d8845f8920ee5936116406cc0c73a281c4a3f636456c8218543131b4c2c14e37f613038722deb9598ce0c63e2216332626826f371ef866639caebb99698c132d7487ef2183bb0798b15911c01960b50199db1ba7e7465ddd0c1d77db9069e2bbc534eec603d328102dccb8c7896530374fa5b14d6fb68b11efdb3d30030c0e26e24f66f4b831f066828c0b649a606d6f15b2c2f6868346654768a28f845b0620e4dc08686806457c8c081a010020cd7dba6e744fdf53b42bde23b9f82113fcd6378682a1ae9ad544793b2b0e9e0c2a2854f2557487389fcd7dc4c1a0383963363161613634343233353034632d570622cd7480ef8e5fc780c2754bc78c15c95172a30f872e8f11f5921dddce3598189a094bea1d7a7bac350ad75d55e84cd475c16ec09fc95df9ebc0c2789f09d2c473a1a7e7a0161c001f153121f1903381699a62f03b0c82c3f14e612d01e73cc1e378d06ec01348374bc0c27affae807433835481fca2c16ac0182b97fc9433373e846e7070e7931fd6e82c4bf95b1879ee7adb6ec0d45d7b99c0c27b96e046c339accdc4d6e047973c2e6565c7411f53b2f8820b3d326722a65e094fc9c36125f57dc2e26ec0b2de1428c0c27d1d70b649b9c6620ee4893ca36872a0be3d9c2c56b251325de006f2613c579716a00f0021807e7ded6ec0d9b4a9a8c0c27f80b23c718a43a784ebae87901fb362a63695bc4d878517fa928e761060aa0846056032c98b7ff86ec0f74a8270c0850480e4f10895473950619b0060f9ff59a265e42a8f77ba95d7114466ac2602111289bd2d59428064461e24484b4c3ab2299948c92d534a5e0c7fe9485d96ab484294cfb57833f90890103210302024d385244a46063a403c0400641c887c0bd7c60909030742a6c18235ccb36a872b87e1503ed7624b4541ce9b9c37b93fbe47f729c5770a9dce09fd9d4eeafda53b7c3929eaf0a7c79f33ba8453a2a0145dfaff8c34befbc709fe7b84fe3e5f82f11ffe2449a7fc5f92d47d8a7253f94d3d6c81d0548efbeec5c2b289ce16e5dcf6eb9cdeda495dd35317f5815de9bacb595bbbeacba9b8419aaa3b9b5deceaa8bb6a8b35a8eadbc9aaa7a3f6f0f53874d577fbbc065d1044d8aa655f7da0b0d7daca772f1eee68965551b0d0b2cbbbdb87b11a18015328c0422822ea41760d374500ddc5ff2f02eb9c612096140d8b4a056101c41a87196581ff3bb88c07050b178cb1d032506cd08a1f2b69282bf151e4052bab8150aa2258c48a16b46b877d851b62c26ef81069cc802639f7d99b408b687477780c083760e339e2ddc620f6b56cb772cf510081ee80048135bc1a5c673ae566030f43bc418c183413c2056d4dd0f7bb0aebcaf6b983ce6407aea27f17ee1e40807523b88e1909fd99f01a010020937e806f8b03ea1a02bd8aeb5753ac9df279e7884ec3da87a70e511cd82e88fcac5f53e12e97231b58f47059010131303002ae40dcefad663e79d31592f104c0c6b707d6df60b57358c9fefb0cb527f3c5e20116490d9822720bf48f9b02d8b489e30a14a10520215cb587e615029d0a00b4ff313030396639323766393564643237356132640314c93aa0ebb8343c00c5c4b8a477537983372df784b74f62d82be1e8c644281a1f8433b7da3a8780013e8eae00c282a1dd62321ba3a8f0da21bd81ea08aa69f76a844392f1d7f2e65762c7985bc1c0314687f0c59e7ec28b6fc09f95e1e5b982c28319143c29f24bf837518ea9c307d3959481c23a1156bbf68c171865078cb5a87dab386919a583926fc0002bfda3a583c285105fe7d2ccb474e93ed28b350c201be8cb6f84ef8eeef16aeff700b320521b0e09da87e8b0859d6fc0dea5cb4bb085c28622006ab513a4670d0f2af36cc4e10725bc2d234d25f6e8de18fb2331ce1221ca97554089b77ec2a46fc0b717b5d4b786c2874479cc1a0628463ef4669ac9a64e6056e382bba641bfe7e199e21301587936ac3cfd64babe7e87ab6fc0139e49fcbe87c2895206a6302ada6a0b70f2d45400d27dcb55347fd993f6a76d60a73cd25fe40d1e32504c30c97ec2b66fc0d565e6cec9898ce94213463b5069db005061440c73c6cc54dc020030185d7cb1fad4cee66ea2a44d5c14a6cc1cf686174512d81a09f86990909024f96534f0dffdc1ad8145ada10b9b6bf1d5b198d09180208282050e1c0e088a0a100a8d0615c2810217efc1e42122810386040048045b945669862946c13436d7e2c880869c2d395b72ff7b099d7b0ff97bffd24de8cffde543d3f93b9fcf1ffe74183dc6871fbdbb7733468f7042f7084de91f49f922f9fe0efd4f0846eef2c91dc1ef48c3918c0ac5b0ed4a2a555d32d79373ff44a3b5b54610aa71bac88deb47965dcc5ada525f0cc5cda9489dd9ea52533fdd525baa3952df4c4535fdb486afa6a2a5bed7a63547630860a9556d7422c05e6b1fdb95683863d5fa982060559376af8d621918085328804238662b0039d519374300bda5ffb7d0089063b03ba8abd8c7f0338b48ab69013fd03606c506050c604c43876cbf381dce4e8eea2cc03593262e808625912f5dfc90af4caedc28ac7c0b388ae91c7e89913e9faec34cc4aa0e383f8cb9b313506351bc47e86d81dc00f64061c50722f6489f20e6780ab8702158b73f1b601cc0f80ef672e8c23241bb90d0c4fa7657e158d99e3b084c76ce2afabb70f00062eb46706533821bfad8c56f8c4c0b901a0c50b2334a7e87b863a801f344fe3b121f1fb19e97d78ddde2ef47e20d90382fc1a0376266356336343863636464356166321ccfbe628c809ecbc85580c28db817a19e5b34a70df35847e4f91d243ddfc66601f1fc69b3931ecc54d105b95b7a960f125bdf7ec28da1cc6fc09f31bec209c0c28e69387f219848cc6d6186f44cce0ffb84a120d9a9dab1ec26a1fadc91abd848b60f3f32cee68ed36fc06d4352efc0c28fb091ce3a882e6887f53477422c2efcb5ce2fe5de78093d31e52dc8868338b4d47fbd3da8ed7ec2da6fc07a678c07c0c290415fc16f95540ae5a87b6936ba749c002083d23ddae2b6e9dc1041ff0f7f00ea1815d45df490e16fb29132704d1356c7c2d863551431f193088a069af2a51a49c465a6d40690fcdba3efa43a3475e08a4929a90499a5bc8feab94859194ef63eb112a722e044120821e1100141f8b8a83819b6e3c96d63843261500f08a992b960269596ea3a5b335d00d9fbc44850ee7db8f772c1fbe8358415018ca88f60e6b8d6d7b84b564f2a2db640ed732f0f38c8b1e56552a16143afe6c87ff5e4cd217275eb65c50f736497bc45fe8dd514fcb09d4f91c0911fd32c6f26196f6a69cd10e16f65a5d85feafc093a41f041471d74647195bbf70690f4648f69936b5804cc70bdec90c2917d6cc25d229bb7fc531fca311eeb4b2d1b373a421f4132bc6cc17496d59a034987d18788fb7ec2e86fc09f517d5b70fb91c292443c383bb8450169ef577aa476f018f839459d7fd11997b6af697bd7fbbbbb84c642425d827f92ef6fc0afc5da8fc0c294e0602add78d489ec3eff4a15e29b7bbb77cbb65be91dbad473e84f0c4a1219bf6414b4f18d94fa6fc0b4403308c0c29685bd640e89368208a08f5abbe0c5001235f43bee97d57b9aef11aebb9056592b49473796987f968570c0eda7c5a4c0c29763e4af3fbc8a76e5d1b7d3b6a59845b05750c5ded0e6c1ace26ed302f44a089f4a1c91669f978c70c0aa502341c0870498e6f208a50e002294473b60699b00a081524016014bea7e86039e8741a0559642769ac4da962c21d55a46a26b8cd53ba06272804a01995f6692e9cf48423d5042c81a4059140b5aa5d68a6c2622011e2868099c02101e5836132e0e4044d81a5018644c1510442890e04062a3e261ab29932666e725b9306895dac700877b29f7522efb73191b42e7f2bf65f7f30da17fddb2a33f10bafeeee7d073d9ed1abec73dc676df0f497fdeedb29ff36fd8fe0d2177effe40f169f7dcfb657cbe1b722effe3fc997d684ce6756d15224d94008ce5dccb3f5d8def71ce961c8a5273ecc0b07a513a39e27a9996359388314ad5a9c20e73c474624dc41543cd0c3b9cd99ab1e088abb565acb90d00e8aca86d625868a5d4b1554866319524392808545425abd626a10b0c045328b0422823563d951f374d00e1c5ffefc40c113c76b01020832eb6420f8f6fec44c75461d4ac62af5cff809656303e5c300d0b3b36c2110cd81d2a0b102ece2d601bc060c800c000b7a2320938a3e8e5a509140070902c3081cf1346118a660f28262dcc02e60d038b3574726d7417063f50356e03cba8110c80d2eff2383b7858071c029861284c34171a3791bedf55a857b6e70e02939db38afe2e1c3c80d8ba115cd90c3b15ebbb1a010020a6937098bce79e169bff6951b75db879d90d994df3c767bdd77a2c9c78fa0c5e4e26c74ea5a8910ea67fc4c1a039363732633161613632383665306138b353f7ae98809762f72280c29a1296b250275dd945970def0d9a981ca6e985080a4e4c27e8a603c096d026bc296457557eb19a9e70c09fccb3aa0a9ac29b2aa47eb1307e6688300cb796e841c0f30db4b1000452c674344af8183f26dda5922d8077b89ba570c01577c5d4b89bc29c0fc5a90f31faa88bfa91f671e8dbef7b1a5e4c3648242a67e34188befd4d77439d6cfe6dbf9cac70c0c9ded74dbf9cc29da70179a3cdd38184a37e9122df5ba7294216cada831b95d8ccb310758218aff00c831d1cc69db370c0f59af86ec69dc29ff9aafa6808ae7ffc63110d5e84ed5deca3a5f3603f3dcd1b27fb3d9a30760e55975ef2b1d17f9fbe70c0cf3745d3d19fc2a0cd1f4858de60e5f1c726a600df3fb298e41a028c494f7449ffe2d82ad93eb291abc68966d8a0c570c057cb0430c0c2a11d6196a2a570a3ed2c06930bd45019f149b26e6c4b7d5e08897596285f73662815e9d1e2df7fc2cc70b1909ce9f45f5545ffe981c1ee2d263d992693f0942959b1287f73485d492d8a2b2c2c954b2b1b324b9c73504104d26f6d00dabba410ab4a4038d9c540c465b8e406e56160e128c010701d1ab493c1e903844b8592335a4c57d9990a0bd0de254541702fc4bd971d6a8904b94b264b82266bd21ed7fa1977cad572464b1ba675f772c27b1e47222d2e3864136b86fc584bde3c40ae7e7db4d83143f6c81bc89f5dad61c756be4681213fdd2c6f3e2edef3dada32e16f639df42b557ec5ac9e49347998a9a1c8b3fc452f3d14a73ddd2aef242695fe7fb1c2a20a8df653a6d6ed73435abc5d0d389f93a387c3abe5b8b69e2221d401c35b4446178dab08e6a2d370c09faa9ec517e6a2c2a4d47c0a8ac36418f277bd580c43ecebee8d9ba5c6746d6c43733be49c703b63278aeeea41f1a4de70c06c84aee4c0ff03a5deef08650e00e2934434606d1340144d61688f35d48c20414c324a22b60e6ad1e296ac50a39691bd61ac839303c700a4cc24534a7f0e35f89707bb0c0805009f6b3146606190584c3a3e446606c5c6012ee9be2165808346805c46422e3c0a280c08167c551b55355c330c27e0732db6404c72d6e4acc9fdf0ff7df429e1fbbb7bfc771ac139a17c77497a9153979e47329a1246f7a72f7d7ca730be9432be87fee0471175693e7fff2ea17f7c28a34791ba4f28378ddf74c396854ccd32ee5ea8ca72e86050ce3de7bbd484734e3aa5fffcbcae68dacdacad3df5e554dc5d666acd5ab7da34d43db5adba9bfa76acea69a8377c3d093df5c5b8adbb0981065b2f8bab0a0ff65a5be1ee65c39d8ba22a061297b5d92ec661ac46455328a042a09ba201973b374d00d5c5ff6f234719ce30f80f707db08e00d10e0c7811995f22b2171cc760ff419bcdbfc9d6800818832f46e44e1f35018e61d086a3eaa62dd6294ac6d50a73eaf68865148251db4e132d2f705458121ee2a9c03a86160c8877438c984756b1a7a0036c3ce3f3f27c978be54181e02be31371c7cc93c386c31a98ba408c052bc431420f5883f7a8723b13b00022880520e10204b20c8f33d3c8710012a00124c04208c93200005008070803020080020c080000028b4034d60c2239715f255d8a1b0299a8a6152357b7e8781163916483b8704a4d548689e8239d951e6508dc662e8a7835914348e4131879d8489cab17239242819290541f423a066f7902064895d6fe9c01789352ecb1aa47625f63da7123b881c9fffa54c4c7a34bf000ddf606e482e40a4f7309148adf5d7335510a9008c78a51bab008102dc5e8760087b1d4445b6e3d00e9d34231dd0040328cd7ac264a0113e1fc46412a8a72ff61847ac6d2f6909ac8a8ec03907c6df44a5802c5e158207e35d11a9708e7370a5351aceb8f452930d4b384d444cc3abd1f6958b9972d01a14e58465f4d54424f042a18d5d8fb417866d119ec4420920b3dd26129d1f3c0ad491d91c773ac8932b9443fbf519c82c2b8782c4a1543df7b484d64d4f5fd48c2ea5dc912083457fb2fbe9aa8862752d71bc52afcbf48e9bd6311f5cdf87e3795539c8d7b63bc84cf8fd45db9fbc3e039019785a3ae86002926e0b336d0644a6e9347ef1639c66034dba4970bbc48fd1ff1558ceac81629862308242d2aa7388b3bfaf864a420304a5a2fda4b00725d07bb0430ff1b485a110a5d49bfd572c7f428273d341a423251a715a3b76e91f12263d116163ab7ccef479a56ae758340a84fabccbe9a88b49e2874c5688cbfa871cfc388c266c96e484177ebf004244f1b2d8296403934cee55713ad718970ae1825978bc0a26516d9b1d04178297ea4b1a2434e02f73436a7783591474ce473c508fd5c041257c28838e805c15313a9d8fd0424471b7d134b003834cee25713adb9447a06a3178bcabef6582451385012922911813edcd3f319e482e482a7bd040c05ebbeb9ba070568f30300076de3a60fd1f7b602e4155f470fd0596f873e92b5d23067802d189b4f5c4dc41926ca79c5082d1601819459c4a1608c9d4a4147dc1c724172c1d35e02064577087335110a90e8c78a11fab0080025655197baae263805bd4a1cf3237556fcf124e014b008f4d544a861a29c2b46e87211085c8945200713674e494c4abc36c60bf9fc489da3d03d3ad2301d69e5edc4644269c6bcc627e23756729503045b02e6b581dd50122c46f46e91631c46e79b65e20d2d42ff475d55af38324180e128426a1495170adbee21c9292309e151627baa770400aea3d125010d7d04b5837f259a2efe5b233ca227c0803e69d72412158c48acf5235066113a76a230cc092e482e78da4bc050e8dc9bab895080443f8191c746e4dd701819c14ed414cb03907e2d7a272e8175c87dc8af2652e312fd0c462e362a7be1b1c808762266855e17a42d47ed258051e89c9bab892841221fdf08a622f0dc1d8b50ccc00f3da426b2aafa7a2491f5d2670c42338995cb644d94e513e9098c3e2c32f7da659110ec446da1bc1f695c5dfb8dc17032870fd7d544457a2225df6895aaf34576c9711621fcdbd973a2a4aa4af81d62857dba20550963298175214b28b3e4b4d4fd2d07393d815cbc010b9213067afeb4c29d04d2667f649acc8b0aca35a68d8dc0180ce7a6a56f573324bafe774c101287a89a16b057cba1a5fba8653092213341adaf8086f4ecd9b4daae9f319da3ada328deb6586286aa4ca947d214141565d48005c1bb452228dc769f81d4d4c470938930ad18bd738bc68b198b6c6722808a29e85d71be1f695c5dfb8dc170e6cda7ae2622ae270a593112f516e9ba752c02c142378bff7ca479f5ba139680d3e0e6aeaf264a0412e15831423d16c1c2492cea14202942a5a0a7bc00bb203d016a2f010c85dc3a7335112548e463c5087d58041099b2a85380a4a84e4147b90bfe48c78a0e2909ec35167dbc9aa8074c8473c528bd5c04892fb188732001944a41cffc12fe48d74a8d1e08ee331645bc9ac82326f2b962847e2e02899b58543a78dc724a6c5ac2b5314fa8f32371567da349e0d45ce7abc2576140d313b05f1be8722597e4a3778b1ce318cdb62be2012f52ff8f7815673697e1c856e48b4a6cc2d07b5272ca48426094b4bc349700e49aae860a78d001072f6a6f4606b5914c26d1e9375aa6941404388ba2c52cf25ca526928455f44816ab0a2d10ec668664366ba21c9f484f6f14abd084d79e4514de5c5ba346355172ecd6479a56ee661b20a89bf0807b3551819e48a9c1a86297eff1128bc4b01385b045f048174bc79c01b7d34cec22cb9a28834f847330fad808dafdc7a223d889b86a7f5c9076feb597c0a2580c345713a100897e02238f8dc8bb712c32829d8859acc70569cf57f108600413e4f66a229a90c8e7370253b1dea2956bc722547bcb4888295ba406dec61ce1ce8fc45d75a0cc26e09a8022edfaaa0a506202be6b035daee4927cf46e91631ca3d9c61a3ce045eaff11af62948eac4831388230d0a2b245b2f0de939c3292101e25b6ae7a4700e03a4d31043a80c0829f9bd58bfc3adf32be7f8857eae8d86c4484c4476bff11dd8f79fd5bc91ec2409089347d23334541082463e46290929692743944a8d58397f40120d5dadd4e5c02cb86316fe838ce86195c93a3e0262d9de9915b3fd02d74cae1f19ece5f60049d875abea79ef92c68c6ba476fe7e56f8f133580447afe46318548bfef304a8df91e3ea426e2d4f90948eac71d61ab9518b76b4be5742710c71e12dbac89628e7483b4450f7c91015c8202cd6407aea2bf0bb70710b06e049730033d97aaf9e6708dec1a02cd0a00d423c486cfd54bca7138b519203322327bf46e43a780a237d1c5bb0a82f0a06c1d00003d6922d6c6fd161a76cec23b11067f22cac6409a49b28b905989db64f5cb05a50115e2b80578c84de06b1a4ee492f44d6508df34d713363e8969af84a59b18a1103c173a76cd9181b690479685d93870c80399b7edf986e51502950a00a4ff323135373837373733376365303737fd310114c93aa0635c5a1c80ba725c92bea90cd19be67ac2c627b1ec95907463231482e742c7ae3932d016f2c0b0301b070e791b1c48a086a576dff3bdc3a6a108b3693774702ec8c70f1151181cda06087e2c7be451ade889c9960a3ae7ce7850afba8180a6edb302a6916b0a331b08b29c99f90c8a389f209ec4cd2324494e294971a2915fb63a9b5c83de111f5422529297a974761a10e084dc080500f63d4ecbf1f124151b142017198f0c9288087418146408063234a25927881139d05c3051c26a358dcda9b000fb1e3110927b1deebd4c757b4749ae7b624993abd8c6943ec7dd727594b04e0559a17b79d2a2c614676102434762cd8f1febc859c363ead7c30d43cc8f1d72f6f83b577b18e21adf83c08f7f7e2a67211a6f797d7ae1c1bf422ae75f68fc08594594a39a556989d236dedc4f9105b23f3f955b4c0430c8ea53b2c3a7aed2ad1ee3bae52c3157ab07265042252068a58750e5503f26fc33b9643b59a2d68e379b87a7f4c1a79fc5ac4f3dc09fc3a9536ffbbf0ede1bfcaa0361ed1dc719f818c29a3f36ddb00c0a70cf516e8b21740cd8bedd92a9ffa97099becc92a9c3aa8a8dac91d86514eb22b55a52d92cd28df9c01b60963ec51cd399f2df9c64c79f6a07c46799aa86aafd581e5a99aac3ab10c5a5971e2ed916aacf3a073e49406ed4190e12fef766fa68031994be9e60a07050544ea0ab8dabc044cccfa0abc3ac12e377043d0ce6630e754c16f91eee6b5304e3a44f40767fb7b2c6d84f718a8f04749abaa7ac94ac8661d6e3a7acc3ad3fb1e1bbbdc7f0b04bd687c990cc102e6e769d89bd7c77a684bcb10ad7e51ba5d4cfd6f8aead9bad14403ee4aeadc3aea650a176618c68aca4640c300716601abd09b191454ae5b5977c0eddfe32356b8cd984c2b5aea2ae3d7f8a3eb5aec3b0899bfc3a1e6ff3f9ad58e74a60a45fdadf300501abf063e631930ad1b0b67148ffc523a2c0b0adb019bcc8fec0b09804b2f6f008250f006213463b60879b006089410345da8e1b08a5a59e8c101355f94052701b89c4da962c5102918cc41b95a33868688a8f180132a54c32a5bf91c6be50443f97c08fceb5184b40e0e13020d1d051e0338041820c000f0d22010840d0cac24781e7c006c5828c428583045fd546550dd78ca27974aec5920c42ce979c2fb9ffbdcf97a83f7f2fe147f43d7a44fdcdffe70f7fbac7e92e797c1af947e8917cea2f92d274e9e5532a237ffef1fd49cfa57497229cee12ca3de1f7dcb064c252b38cbb13aab22c3618947347af77ca7a21b4ee10c64ed4109a8abbb34cad59eb569886baa7b6557753df8d55390df5862f27d2535f8cdbbaeb0e2ab65e1657232eecb516c2ddc9863717454118302e0bb35d8ca358011a0151208042a89bb2013703437671fb1f77344a3c09ddf17103f7ad1bb1df316e0d006b00676e143731fab6811bee1bc3326ada19e1abc18bb9d90c11658e1b005c6ee44a133eeec630b80cd22cb2625a9d88e61391b158516490062650bcdfcc3ab7f0c663b0273641bb88295664d9de4208e2659d25c62f5a801b931902ccf860f0db1d906cee026e2c83a184bff119f414430cee7923e0cd02ee76c6f932ae5bf4beba45ecc10c3218703301c605304d606dd82a3c85ed0d074d65276ba28f843b0310e1dc08e86506858e48cc9704b9b2ebc50029f6ec05beafb005fc72d9155701727baf21e4bf6b8a32d3f105566632e581ac026af65788d0adeed92459394daea245fe479c7fdefba7d9bb944252c68cadda18c902cc1bb905fac74d016cdac471058ad0829bca10bd69ae276c7c12cb5e0949372142d1f82074ec9a23036d210f0c0bb371e09007bd7712798c03eb88e636343431316434346561373865613136032dad35c48781020165459ceb80c3b3a13d20d9d66bc0466825ffded0953fa5a528766b58f043a0f95cbf5cdb0d5ea8f94054498cd3b3c0b31247acfcd3b3c3b417d8067f671c76184dada2634807cf7e94e1f1795daa3f47edc32e9da0e2928a80d6ddcbdab4c7b40951474edab4c3b5780bafaf4721ca4270986ea51e659951a8912c2eb99fb1bfedeb753b023cd4d90dbae52be1b5ceb5ceba39a5c09f83010a29060009c6be1d5aa7d03e35354bd0ae07b0ba1518c966fa51a798fcb7034ef75b578c124bd5010024027bf5c648ccdd5af24d7e60df042a9c08686475756964d8a4185b1b00000002540d6a07003e8793ab9ae1030e061c879aa2857ec850d10401bed745631a010020d9c66f9a34a1070050838eaa793be060c071a8295ae8870c154d10e930f2cbb27f9f709b24b97f9ba67084a99f790b899293a934808532993800b7c0ac7042fc2696c36abe97c09952b291c35b9c0877a827e07f16b2124cdd0100040264411d44ea78ea08630e419f1cf852231b543440909d5fcd84ba96071864c93c03c4f2e2ec23bf400d23837e4d3dd92193737f46d098026ef83e7c00125e6d0200e4028c5303eaa26202d6466565734e8485d8c0826f466c6f7753f6028204781d2e6973747261746f720a005110849ffd53dd850a45c1c0a63dc673250777df06a01632543441add234441a010020a5926a7d0a290607fb6caa47e9aa858e462040e4c631bc7593595046a76ddd48608b86645018e9004e1245a50100f401668b91e2995c2e9a0766d93b6608b72e42616c616e636506005f7300807040b2410d1afafe18838a06085d5d5f807ced6cb2010a290608e2c079717d772068b3d40f7ebec00c19a50b6bb8886f4b77e0ac50351cc6060098127a4d030054059672c1aa6286e002304bf849b8406280e692ae1650f64636bad7bcd7d0ebc07a7b1d0757126b773e43df36f358e638651dc3eddee7b7689b681f8e3cc27b0bee4bf8b53177705bc680c7cd7b33a902038203e8808006002013eb400e60b89c722001d519547c40192474a7b29470b3ab0111ec2ceba1b2ab018e7d0a290609906805818b85c1af50ec485cf58cec094b3ddd2d7c2bcc3349d62672a06977001402024e5f6e616d65736556616c75654b8169466c6f77546f6b656e05007c19230286cbd98582c5c806150d105b569dd1640a29060ee615f01dd7716112b4bb19f4b3d94676caf4bb4079df2a21862cb8304e09a332122cdd00004894b84d0c11a224020105009f8700007639bb60a3b36c50d10001461873db1a010020fdea6b76e854253baa70653cabda97c8158574410f789ab1ccff90048f038ad8d32e1300123e6d0100d40155615f5f7374617465460404009a0e8d340928772e007a1b20038efba40d05ca3e4146dd79ca6a1a06ec411505947a816b26071a625994c77ab46b320839e9224342094670770bc77b4209b46c4e0a8ad7f02e5b0b675e1ae3c37cb06d680cebd65200897df66d740d08eb5ca0ce7dbb6e800e34c8c4ba947e816f8c0f0176ddf39810119b2bfba511c8fed3cefa7fe77077b212b612b709b2127e0a29061026b8cf20dd685e1f94ebbbc63ca577b2c19cfb919f00c8be59d7cd91806b6600b2f4013241cd1480d938cd7d7b0a2906113b61461b940fbdcc6bb963fa8e5767f75b9135632620e663e18a8c9e040d2c00124395010004054038a8913c402d65a86882cf3d868f1292510634392c5f94133867feb0828867420a2831ff3ddc803ae48d64640bca32227bfe92a4f25ba12f2cdc4af26d94865e99d098934e749560d9100b1848f4c923278e9815174c0b502149af9b8ea4ce3a250c500efa260ec3fb955514145c39805db0d15936a86880a1aa81c94201c083682c5002799867498b7cf86c6a03ade8fded947d816e7a0419de5f52ef7ddc6e8605f2523dd8b87ea56f9206cd06a4d4837ff06fa0073483b1f1d97fc670b3ae0881069e73b6ae08a36f0a2906138c987ad44f0f09db2f3d630cf761a5927a8dc3a290d722ef01438e58fb508637350100780700e101c71000201909f2d0801a195c5d4636a86880d1bb9d97f701be6d05009408a2634b6579a1684b6579506172747383a2004801400258187075626c69631f665265636569766572588200cade0004d8cb82d8c882016e5661756c74d8db82f4d8dc82d8d583d8c082486946f66f2e81d8d66d46756e6769626c65f6762e0d0032877bbcc779049cbaa1aa5d97c639813849956a5403832131c7e51ccc394c32f616946407b1abb261abd2eba03fc7c18f3dec2dab36967b5d54fa9d1ce21417010012bd65050084f7f896536da812d755b013048b4309932ad5a8060643628ecb39987398641e0fc4ca6d35250100780600c1c380441c5e38823c282858cc6d50d10001b218b4b427d9d5c89da91761c08c7e736490667cf26d08de073a62386d453b1ba95b82ec283f88a62e4f1f7b24dbc28f8d632320e328105cf480c2158a7e8a8e2a8dd514dab91db73b352880242eb5085b055f79fc4f2e338c8e838a5143d80c976cef4e789701f5115d50595836a86880460f1bcb7101c1d02f88bc7da96e83021f7697c1a67e936f9103bcffbafffc7ee96fb3a7045b039a6388a704f5b41b0a0ddc80d8954bba80db3a225b9fe21da90f0710fdc9d353f98791baac6856b2127b5533b18a4b867ed56a4963e34919bb391967bfe2174692d25ccb45e37138adb6eaa1b71c2dee8aaa23a41651835aa3e27cee61caf4ede6a1217153c4538a0b34b78072e04b4d6750d10001af1151cd1bbc71411812fae427424060ffa3fd7edf5601c68c8e0653da84b60334b639d026877931736ee77c3a0713c891aeab8e3644675e2ed707f742112d516f9eeecd488e21880316f0065cedbc515d70e4669f80b85a8be393c8c50e8841cdc0ee9758beb5ed0d8555aa8030db930d2a1a20ba7c76df360142809935907bfd6b47021852b17ae17bce6c550315c3ebb5a57c926d6404fa41a390f37ce06d73050bd94fefc77db46e80061690d4d98d07f213d081e07ecd6f9d0813f3a552c77fb470b3ac0918090763a8ac0995730a29061bc6bbd493184d1b54027346b20186f2d981e20d042490684aec7eb3504004ef0d123b5501007401754aed9de619760500c1c301928f200f1a26fd731b54344009d42fdd640a29061c663bdb0d67ce0ecc941c3818bbf7bd8b813749a7e9af00797f7f19d8cae5fd809f927f95dd275aa026a3b241c50704b5b838dd730a290624c7c39110dcf820cc07110398827bc39311846f38e0943b11980ea4ee8745360080d464f436a8f88051931c53760a290626ac8597a19cefca8474ca6cb1e527cc41fbeb5faa169977de08fd5f8323d975740170dff4d10058240691200f0da891011485ca06151f1054bf6ac7640a29062703480570f33eee09b29de8f19c67ad6375cfa41f8eb47cc3d50734fad3fd0700686da1a37b55d950e5c058a19d5feb6527730a290628a6bc6648bed8f7aef15afa2c06bbf30f48de18c11e96a883f83b7bf957cc1c4efafe6e838a0608340a61365e0a29062a69c454b326ea1958b49f5ca202de7196443207b2fcc17c833a71990f66feaf4e1226ad0000408a0300338221e8fb630c2a1a20bab1ff155f0a29062dead7f0c4fc1457374b17aa9460d68696a1b3556983fa8c32c44d94df7f7e18001227b500003044757569000f0400230299902f01e5ce05406f0304a45a01e8854de50100e4024001024475756964480f04002e43630e2ce611c6309d0d135b7cd993dd1a17f2ad633b261bbbae31c62c1c2ff8da2ee27acf6b3220491297fc362102b88e573a22c4aaed47a37b906c42274c322d0f43280bf3b704ce7bbb6c47292362a95b4e2d24141a67502ef720c08d502e522ffb7e50c4967c836d5530042ae382583134ce3a73b47ca16d5b3539e658c25d3659e5c4aace7cbb6d5f37a5b5eb30d97cc66d61381c36fc6ce47cd16d643929783d48663a3c7c2eb8fe7ceb6d683e4b3376956a3f0c1c7c426c405960dc989f7d8c6e6d41d48b59bba67d936e6f42fa9d4850b17d9e6e7143b4f9653a73447a0be9dd7448eb9642097549f58acddcd57dc26e784a8520ebd6e47dd16e7a4bbc45d8627b4cadc591c1f67de36e7d4d08e4856f817eee6e7f4e5cf8c3b18c7ef96e8053d7254b798254d5d6232a9f7e8c6f835549312ed585560f925cedb17e9e6f8657b64bf9be87585e8a3725bf7eac6f895971f704e7ca7eb76f8c5d8bddcd068d5ec7143c828e5f921729fce77ed46f8f60693040c7ee7edb6f90613c709b37f57ee26f916269450e999263905e6cdf946470fa1e6f8e7ffb6f9665bacf1d40997f8670976665b32f97a07f8d70986a235676d69a6bbd1db91b9b6c53872b679c6dfbcf23f2c07fad709d6e1fa3fbb79f6f60f95d41d27fbf70a070f944943da17144e8a09ee07fcd70a272a9cf91f4e77fd470a4735ff19f52f27fdf70a5786e1dd9b760a6793c76856281a679ee60a77a7ba12488a77aa97bb88a9f7893a97baa7c357a9c3b9aaa7c87ab7dc52e2690a1ab7dac7e65fc3c8fa8ac7ead7f799de2e7afad7f9cae80ba81af6db6ae80b081ae610705c1b081aeb28635968141cdb286b3876fa1e72bd4b387c1b4882da51b2adbb488c8b589e1a33afbe2b589cf2e5c48f920819fa253b5f9002f5ea87074e2f4993790a089de13f5490144a83274140289c61aa644235046419f1c3e5b1dfaa41a8682124eed010024a1081af4c9e1b335b241450304f561c483a2edad7fc185a1b1859e9292f094a36dce472294a397a297a5a587b3a2b3a2b5a3b5a3630a29062ed10f6f8a460176c8200af73666435df30a7de6b14154d1ffe2e217b887ab3c0026122bd5000048b6271b5434400bc0a6f90f0a290630153f8245da01e7da2adc04966bd0d11e8ac4b60307700ba28815ab35d94def000012c00f0218753d00ca50080e39406d1800ff5ff7777777d5a8daaa20d26f49b27328bd8b0d2d52ba78e94d5aba849833288e60d676b1dd5eac372547e2ffb34d78d48d39d708cd00cd00d600bc8f74e2a4daf572894549bd93ea0be72722bfed969113f69dcb4bef116b4fce34f05da04f286c9f78e1153ddab41ef771a9be07bfed6c3ce08bac1fafabab5adeca6383850a960da817a2f6e5414eaad1789ceacd9e3d273d520c6a5478e9bdf97a555ea9fec47b71d6b6bbaa5fa047dae9aee6b23d6ec62767de68bcced9de7bdea4daf951edaa9e3deaaf6f5d8fca5b416d8d7ee96d69657b34d07cbdd6f29c71e60db38abf521c9f9c9938200470d0cba5068daa169d12bec6903d6769347a8d0380080050881101307c08fc366e53685d7a8ff1b93f7edc2b54aaffe21de7131aa162bc57aa533bf22e5facdd0bb3c73dbfede59534bae340c554d2081a8d62192355b64814503cac25c32f3a2210deac981ebaac68816a3d2695ac54f648bbb79270c7f97f8601812e8b612c28164f639008143c52f45074c102c8e2a150a0040c220a65c1930c3c3caf1e94fe86cfaa96619cc7572c298d5736853ef1b6bba2c23bb7d9b5c1eb4585f7e8fdc4dbea3472265eec55779b3dba4d49a2d0ee4aecbc59459c8f345e1d7973ca47fad1946a31a94fceac52fdd61d471db4313eeb8ed311c99eb33c0e6672de0c66923d2745ba1a9d3ea479ce3cc7b8b492d3788c37926e489b6f9d2412975eb7d0fbf123773d081291b69cd61d38b824923d081f3440e2b475d065399106212005305aa7e6ff23add32b9774431ba74be8ad4ef28b1815465146470442865e525fa09cb47adc080191447a792f89c4a8ad65a57249476483850a4c88642b7d08c3ab15fe95caa5fd7f90b7c6f3256db050c17203316634d4d62a615a136783af13765cde2aca73064c6c82d7dab60dd37aef2fcc51dd6352894dbc7d41d973d2c41b214cc8ff3392a86a717ef1fa13141811c53236110a8cf3568e91dea18f3f87ef79a502d278c51de74f53f1ca29de40235378d3d92adb31d5bdf38e23893466e8d6b29218035195b796dd5a4e8e93b1efa4b2c78bb792348a7ba53858421d946e134b89c7d6094ff33c159f5c7ae5d05b9d455c754b65a75074782a2ebdfcebbf51085f7bf33eeb2eab6dce81573c4ff37cf2987f2a4e2052b9f45627c99e93a062ef7d3daa378ba894f18e73f138345eff0a28cd1432441aaf868af16b4ebbabca78a77b696adaea1e1e7fcdfb8f9650f71581bca8b1a142486404000000000000210400c0502029a9b60e1248c234d2a31c42861183000c008800022400080800ce8079b43b9a1203ea68f7a0f72046e40fb099a6c54aac6dd365568fa24490c1c82779220ebe406b23e010201c66ab416384bea81817fef28103f40d440e4414f4258c16833b014c9d384e80567b3283089382fdcb4e22d3bf7e04d2c0085bb857f489b33600e0603f7678301b6ec7120e882458097d94284507d44bfbe6d5a162219800d537184fa1233664eb846a10db8a046a3d7757fdf5acc17084bfaedf62b3b7324fcedc19ef982806782e5d76514bcbdfbb9c7c7489522aa8639ad265a6cf726da753cda23a0824075f095ec351a801c27952a04b288628103b01becc5a1f9bfe8d46b86c15006b9de4f3111ec87286cd0025dd0de7461f24233182b948444e1c7f98dd5e8400025093a38113cb13dc2d00470b14391cd7de446bfe4c86a77812012c539918bfd06cdad9b09971be73751102b61a5578be23611053997f6acc1eb23d0dc4d614110e38f2d751b410d443049c096e230cde0351a17949b21f75348eb15919ff23ee723312d0199945470d7435156296b127f213c465929151152c4f24c0a292247b44cf76752f256eb9702bd240297f2874e9c5587c03e91d3e7227e48c6d12714ee8e0b20984f360b30897e09b031ce00dee2f6d89eee8bfea49d259b25093a065ed59086feb67e323c51880395244079a8fce68d58da91a0d0d269b5c138cc04719e235706ac49c6e70757ccaded9163b5fd66e02a7b2a8af36e02e8219a27c72c973e73943e7e7025490b78495129d7e920bf94b4bc648d9bcd2ff1577b8dc9db591ae8f4512d761e058d04bbb5f3fc625daa632121af0b10b9eeffb5f9c2a78ca4eb986eeb78b5720236b92a7247cfe25806a7bce4db9e770e2e32cf4e206de0fe54691d24eccc4e1c5f0c1cf1216be5624a87d1b43323916a7b0e55ce93edcc0229571d1ee6a940003a4342e5580984be1756c90cb9a382e16610f276c8f4f57c057880ad74615501e456cb552556ebefa32f4b017b2cd889b8ed1eb16dd6b7bf508ec3c23b5ae300ea0c8e002bb1a196cc525f6422e64833d7a64011acaada1b6961ac2d34bd49867012e1f1112f0c4f2ccdd2c075389327f73a72df23cde993881e321a732a2380ccb948fd0c3ae65cc7ca63e72ea746a65008cd9f1de890326b1afc91bdbf63ba84834ca67a771f05673f6369a35026dc6958bd88ab4a9177bed43bd7456091afc0e1412724ab64f31500b2c959a7471385b274b85181cc013bafeaac8a394b813b631d51b712de330bc92540111a8b13e548d46a52f923d943faff71709bd31836788de72512f0586c6a55c7d581202a40065a5b42891246952a147369a1550c6a86b81e98c3cb08f2785925d306c7443fb544023a91d0377d0bd8901389a32c04f35b91e6ff97843441634417a2ec96a2cccd894dcac53456121c04e5f16f48b507cb105dc22314802926a600a29063429a7cd94ed4405656955c22c349cce8908252505bb4b73295d77a1d9a68327001228bd00001e51030033823628580c6350d10001ef0f2df0640a29063628378b9eb3d72976fc92376e30baab9c93113c423523833a4c380133b8ac964200458c6393bfec100a290638389f8e695dfa4667acd7157e31fcbf92ecab51088970ac5e78b53b7434f5a1000012b3100c1b0d41006a4b040d38406f12003c300fcc0fcc516cb10e3946da527effcfe47d9e20ff65f23fa551a841dc0c3159700abd4f883e454a0e8c6880c67c3809e83102cd00b300bd00ad5d86d5e8d93097db02f755b63a965249702e9b521dd7b8e87d1c210544b3871f8e196c6ab5669e0d2d94a2728432d4d60571a9b231db73a7b9fa9b2b7395dd1ae59dbe71f57b13e3d12083443f3c5ca303c3e67e43cc9d27e5b3d94e49f2ab779b23dc87f81393a4e7ceb039e495caff4f262434de0fb65e7eee3c693271e03cb06464380c20a88c0c071684406279c40d7a32472ffb3ede8da548551408b139c22119661f4d2a7e340808b0089c5030608548890bf9187e4800294d212a8509d70d374c8961a19812c393225fd6467da5327178881da718af33a480d17c924373d1522986db8686db39f79dca5c86faa4e4def1a6297ef5b1e3768ff55186d1f18da14f98396fcbd567f78bd5ab5c85523d1b4eb91ae23749f6f293cee273c701df12931969ef6b6ffca624e1ba1b79723dcf15d2a8640c9f6955d5f2f3762c65939cabdc9618be5c2db2f6b30f7aae90bca0ea27b67c889fcf62bbd52fde65e3766fe9ded7831e95cab6e774a8cc306348a9d0f2698f5db637dcf6284c1cae31079fa7692ed65235feea9dae7ebb17bde3cd6595317c5ad522b71c819f5a8a3c30384eb13496739cfb1793051ccd841b7088b1817927a330a4327a6ef9f9663ab812342d15e385779849f7f049e7bde9b80f72f75ced5a9d52f8a408878fb1710b6c24b4dc0cffc768265c631f74e749d97265d2b16c3dbb9324ab9c582677dbeb6c6f90372a5b4bc82db6fe65359d2997a3e76ac578b937356c2c45d92e1ba471bd19bd4b43fc5b5eb247f0a384a38158ba09895b505a5733d20e93924989c30c2fb7f8d6c8c382c252a18bfc93ce4bc3ae5022949040b031b2ca1e842eb22e8fdbea2b5e9dd145449eabd3b144928d6b6a65eaa084cf1f65ce938e1650a582b141490cba469f1874578b96c6b2fa755ca36335521c9dac713f4fa5f3dc27c6f0d1c688fba1d2c8660dcf35b1c4683f0941f90b57a94cdcb6e74cbe605db28d038babdf2e4f47e5816b58582811fe67932e3acad5bf70bd1af56aa451cb4a65d2ab11e3b51a29fc846b645243e3c6d64e5723d5e13cc9d25828fba094a6be068216a86284d229a54664000000005208000684026aa0acc20f1200f0902810931c73482900c108000000000800940000a98b3fa87e3389c3b971b35d72ffa77ee63a7a30182049753730a73b57878ad390800d319c113a2fc88e7082985807f912f10e84a6e5e6f9dd1d12d3537171959e7d2a437faa1a9a97cfb325832da89ac5c8fc8874b67dae8182918e9079e386a6b24bffa8cb6cbc4c71a4c0481ff6f080fb801ce742057c86cdf63997cbef3471d36e49ee3122aa55f8098d532ae122e968612e1f642b86aba1a3659f4eb0c992b0dc56de74146c3f0f6d28a33a8cc90baed16f03b200d841ced738ea7c213487855ab736d92fe3aaf1fdb1af0d98c0c2ccaa41b23eae72851059c3ad43ff6908ea035e32f7cae4e86f6c6d7cc27e65305bbf00a9cda79ef5d7f802cb4e32a28ed1bce6108da44b20b7b7db3838594d27ccc1b8c9772721640b934e41ff9c8020f7794ba973c06c66e444bdab999dcacad49b273de804ae62a4522c94ed336d6afd11051a852fda8469572a4785e32c044c8f73b6a672dac105b371174c4aef94dbc4f792a8e9dfd5ea4d499001618de15db415225bd11c9088b9a5e906a43c357f65e2d768ccea2244e89ece3008d9e68b4143a8d718731fe7b6a309100b7e0cf6121844d9306beda2833dbd3f3fa031f8bbd9109082d3de917bc1b56b9840a0a195b35453fd29701da26042af1f6af21ad497ef8f6901e780acc08a4212413bd8b8fbc13cba6564538810f92d0a6d240987a011f2112af7a50bf15bc1b0f0710ac92ea583f476204571560ecb9cce366f9bd7225f7a86a31d248f7a2c72db3264dff263b2b52e7562c8b2a2a75ee8c0cc172107d379847adc95e0f70dc81337c797adad98a32f65fa38000b28346428a54b1304f1f0f1726be91b408c504f987810d41e01e2e0e36b5f3c05d711d2531e8989eee836ab1003b321925a9b8631a87ec8f2555f8e0fb162fd89017717443d24ba1e4f006c31cc4923a8011b718dcce18b5e728f3d9904e8c88111ce494789e9dd87020c7a1804d88f923c9fba914e96bd6d928d773ff13e0b6e6572ddc8650c95e451881702cba60eb55aea0e68667d62daf7881e966e081e6673a10c62529b054aa4f722f78dbd26e2da2c1333b502f7ee628379acd98f0bf956b2ece8ab4034f241420a6f06188dfc158508ac263aa812db3076a359dafe5394ca4ba00ae9a78af8ded60136d0f051bf7d4738405cfca9eca27b198fddc302704b91c9819ae2a4c81a0f13fb6156332816f487266edf093931e270e0ded65bc7211cd210cdb2c6380fdf137906c48067f6c6542abe4511db64ea1c569d9818f7ef1eb577c719db1295c8812b2892f2eaa0e134532ce8b04a4357d094745ace4fd6eb1006ed58c4ac8990b80b49b677be66123dfe8e4a00af3154a2fc7bc1b751280480aff4cea9236ea57b3a37111136d362e5591421afda69c056ca3dd974140f670aeeca64ff6c8cf4fd0ce646d23734401305594d23e88dbf9cea5f13651867a7f6c859a10f75641244dc2dfb2c80228dcc237a6b2f64c9ee6f6df0571ac5cd8436803e614dd37460373225b89c71617aaeae0d5dec28b4bf31d2678acc11e2a0c8cc63fa4cb509f6f736c7a9ddd43145eaa02eed5522886ee9632db0105ef733d0a55710cf97b84dd29f1afe49319582fa2d5de37916b27b4f16bd62ad9fa24ee96837c837377c96dd8492c0a1920eacc823ee66314862219cc1223dc211e4b1c48bcd3f00ba47400c36506d0201f0ffff7fda6679fe18234a22a54fd2fb29bd774c4f7ef2ff273ada49a1e8b44487c08442039892225bc4696187e9bcd62f11bd00a900b200dc9a8c84985deb2413181d6ecb90cfa7dc2464969b59d0848e7ca8db14b69fb0936d84090263c82965dc42d1da163a49875eed38fc30c74975628e7c1329490822d765a76e304cd887ec9c25aabea2c23c6177427896ef53fd56bc682cb000c90a1b6ed1b0aeb7cf7779d32274149b25085ef34cf163fbcf3eb3a2a8b1afebe53885f2ff9148088be2c772fc4d8b2211064d838a8545a3c041b2b060503907209543cbcfdfbc1194240678c0b68e034e30ad7301808011080294e81011d81f57210463f22c6491109f90746a22947dddeccb532811767fb66169adfa3a09148c292fcb1826dbcdb7cf12a2ae4b0723f68c4f38ef9a6dd9366b9b4eae837ee77d9e39cc5719557fd929349f50f3c174e4cba0ea67df20d8ba0e367ace59863be30511cad6c6efbb62149d3afb70503947c53149b8d63919cd4cbace9b6d841c354fa88c97f578d2db6c47db73541c9cf9ccd2fdb34747af99e6f5ce90b7d91959bb724e8342d11589b607a34c3f19afe61c8713d5624cbcd73c4bd56fd6a0677ce2a1b9d6399d496f1941dc331d411a151897542695637cfb14910418930827c0aee2e2f2c625d92721d498ae73456c4e1d9830a6b5eace0e83ac9d83cd5b916d7fe3eca89a75b294cec10fcdbee2e2133781c0a898fd5f11e1166dcf9b1621a3c2205bd8f96507c14d98591854e5d6c8ef8d4f1232e3c3f4b8efa1894d0675a1a3eab456b196e1e38d206c86bd3161ad08350fbefb193d02919048083bcc2aa6f74e486342b2309f77fc91460fbea6481e84843070f1a1b99c7bdec11994475b56fdb4aa117a47c75135b28d40b04f397772352dfa4d5028151712bc1c9be4f172ec49af32a9a86e19b764684209a371f1b6d312b673dc4d63cfac754ef641dbed92057b2f1c5566e125dbc1e7c153300a25c2945b13790265b82e0ae8a93e7b1c9b84056e99a8481efc478e281a42d5a760ad09b5269410238512694d68add284d23dc22d18bcc0dbc7cd52134a194d8b2a938218a8927cd229552332001010000062080006420135505555061200e0902810931c734a2900c100000000000800940000a96b3fa86e318983b971c35d72f7a7fee03a56300337496f5d30809cc94788034830f05cceb830af951d3801c9acc36bbd77c74113b1794eef8efaea19bbb821c93ecb213f6bcdc6e7e31994690a30665790ff21d936cf6b6062a03a64c6b8fba95a907f0c349baf7c4103c0b70f9bf5801b20c7d9a30223c3e0734a209805cee8cb07e091835d4744b8003fa0316a256c93b81b66ed83d48a858ba88355a5111c001140000200000000090802d31adc91284b426b293d13c087c1b000190a86828020191400884204008001000080000600000000a81c02c0734e00009583180955e01fc4b62fc6ff2b8b2754c297a69e32a1ea2b8b3061b4bc31283381d5c42bab5dd9ca980816e79d952ea88a81f69e7becd0ffbfaadaedaa8d45e6fe8e66f92b025c406b458f700b2696e1f373442570a55b667ae84bec886abdd5af0d35cf24caa2c38f726b78147e57b2704765bf1a98fe4998458704e6c689a8ccae22f0f378d2c750c05042839e0adee4e3e821651abe848ecfa10d5c7410a3e3bfb007034d7af13d08239898d9e63d40700803a83931855fd17f08d0792ce8cf6f90b9f60ff0b3ee19af31bfcf1b802a5c036a2eb88d1ab1148b657c89a47851707135d14bd2c2cf82199dbbb8202ab37efe1d647d91c1e8f02308647d144bbb34070c7bfcb2b1e832f90ae298d6aa3b11a4f24ef301c81dbb49d46f8e81ac6c6a4e00b1975f27de56ce6f411866ca3a89ba01599094416c0d26b1f36a268995704b134359a98e3137a1ad4819c4d6688abbaa305a6428972df3391fc0bcb3378c7878f3e94afd1500bbac7883b134c53d5529e057e8cb5aa1317e8c7cd82df3b9318c7cc481ec6c21d52e866840a98d0ac5f8e1f8a2fc7b385415eed8fc15f685fc3bf9d57cedec24341a93f3462cdaf5e78ffa92d4b3e818fbfadb295943027dcc48788d048fd3629de373aab936ae331bc46658417e11abbffd95528f007b09d8b6af3770c91ea6544ec9703b1b2833fb3a5f76fe902dbdc979238aecfed9a77a49fe84218c1d5d8b90ac8180e413cf5bec283ff954beedec24341a91f34614d9f5e78ffa96ecf9588c693db7a4564880b250c29b2ca764eaabf8daf9496c3422e79b487498c817005679d340fd1607633cc8dc1b7d25966367f3ec8ad370b4f34fd9d634d0fccb6476fd68772e586386804f698207c580223f37258e86a1297f5984533f6e9d6d979801e02995e04011a0d8909ce0261b3c31f73aa6c77480d9e12782f22d06d6394f36ec8d0f426bf522e79120b4f8679ff525b79d7132d5b1651d1316fe2787508d42a3d0dc4de6d144fbf3de3a330acc4cb7964e4c9ec09693afb44d8c98e89e0678c03c36aa7323c064586b4289c5094565d29376132312dac16a410916fea55144572339db44a293c47f9440facd9828663425edbe9ef9104ac018586883fc24ffb5a1312f82916c0ad472b121f13416c8e562331333d55e1bd7d906b232b63c1b31ff478e447902e40b5fdb42b35fc916d2ed67a6c4d130345f964538b271ebdca81bdf7da3d5df26be9aa95b59851b897eec5666b0055e36d8e362b17d8d8fa01feef17ded95610684c53d3e7f82c18745b046a09412041f91dd62ebcc4f7b1a8e0687b9f2699650086d7200de4b65026b6691645cfa74f9480b42efff83422a002ba4bc09d9b6ab93737388bd3d42e3d7c651b8f3101ad32e1483f7195ebd7b816ca002972b0df51a3f552e257c1d99488de883b0bf87d1b8526d4a85ab1ee7a68d095d821c55ecfe76a2139cf65e82943b62213e8f2f97fcca5f56534110950d8c152382d08b60884a1cae2ac51e8baa95ee54717c81f33f4c668a0cf2b1c33ef235c8af1df691af61fe76d84f6e0df2b7c37cf26bc8d5535ca429e1d2fb2016ae1688ff80bd4b4162e07bc66317f24d934ea0940073e4d4da87fb1d601290275e86c365f024516d815cfc143e0bac6579d2635c478244647658cf580cc397c370ea68389f1e66c33a06ee8dc2d4400977bf8694564d8a025e9753d02189c1c2b716e9416e95524a5b6eb51143b7b74eac1009dbe57376c915bd41f3d05650f2b6fa4dc6c62fc58b4dfddf2209a9bdaabcf21f3ffab9a86ac33fd911c9fa1798ae3e4dab95ae59d6ae62db185525e6de4d25483acdb9011afa391218e54bfa62950a0550d9c05ab12284be0846a8c4e1e414b001e31070fec3844c8840323bec27bf86fcda613eb935e4de096aa32a9ca0a3d62390dc3de602ab70ff1599aacfc30e00d6641bd4b10387320a931d8f5d02ca0689714f7a390078845739749f8f0b4433074493bbd44b87c70ef9c5d763bcdfdef63c4e9af80cd4dbc3d64f79d2a49a547e8c8f195023a8810bda69ebb8f4e5bcd91a2e46963489a94cca2bacc0dc24649056696216180caa283f8b180455949f1a8c511a5b6496b4897356e23554609b3b4d2f82bf5bcada8259da0db450686fdf78ff13e674b4baf959c338aaaefcac6114d5b0f26b90b6033edd5857f7fd3445a6fb3a19ecba259cfb3c1775eb9b4284fe33ec2ce867dcd49c42053c81bc3ba7caf330ac4b68f5217424871907029d5b59863636f11a64fc426c09676454ccd6727d1b8376b0020e28a980a5828d1e4b5c995b4097f370144989eb88ca76968353f00f29d8be3ea22905cd097090243a6017ce12454423c9e29ab195f865383cd30c5391133a9169b0a65dad4251163496a1f495d3acb6002c674ce25f6cea0d378116782e302c33825a76ca77a220090c4fd3d601df6ff8b6ee1549f08f344381f8cd66e230d6b782d1f5257018a521e3a8526c5ed1a09fe4b0e2aa4605f4f112a9d75545c51f1531ef1f5d2971da5d673032328d9c802a95c58a8eff27e1ab59c35f3052336714f7554550d815d5e11fc153d64b4c69607464315e29ac52211486f1fd247c3563fa1344e0732fbc694b7e3e336957ad182ba8ee7e60b822e1313464cad530af1a69cf3d2a937506463940ce51173f77846386e0d88fb4a96583b46f63e244ae509eab6200c460849b455654c426757e5a5beb39a8757de4c298e6ea2288d5ef053e98ab67c3585fea66805fda05f754d070c9786b6b9ead5536c3fbfd6b2630c0833b55887eb82e1918b751f09e85fa6f805b7126a04551bdff237c6a76fc220d46c7d6982b50a58eb022e8fd247e6576fe03441466a4b85615a1585314867f84a558e8307e83d1c836e20954a92a56a4fc3f89afcc0efe8248cb44eafbaaa228ef8ad8f08fe2296b25a6333032328d75822a95858a8eef27e1ab19d34f30423390e2beaa086d9da230fe113ec5aa8af91d8cc6db084ea0a4aaa022e5fd2476656ef103442da3966dad2a8a129d229970e4c342a3a54708a3048b232a82e427092bbad2ab00ca923b0159baaa68dea6a270fd2392b24cbbfb0d4623dbe85baf52417c22a18aab8d3e7d52c2d2d115856500e937ebea645574f64311bbe0a8c42e13ef9d81910176ba1529e127852b6ab12015269814c8ba12ab8a727f286207501ced22370e2af34746504cdb6034b28dfd160237b15a2e36947496cf809fe4b4e2dec700b487bbbb5960954f04776ef0d88e9c2efdf413c380a672cc40cf6869738711fad758c20a4980229bb1151657ca5069433121057a7bdd6f345cb6371c40ba83f58e09b0873700845a996d4fb9dcfcad630ed920e38ac357c44cc63176f4c3391c3fad4c20ab1d052a808234bf3edf7b485235e1c77f3d2369377335203a18896d164ef6353919b2499871dd1bf72366d8370c2bdd8848e2d4a46de8c5d002e61741c44cecdf17905d5605225e2915358728fcb8100b2c4b01dcd801814be87831a7a3362924967be4338e9ae8e7ba0d69aac311f9c8358b6d6d020035c8e9da4e9d5684d761a1d76d26fb64aefa3e3e0643032eb8305b6ec0d6c357ae582273bc79e9031a82e1804b266b0993853e41bdf23edb57808864991e52d288020b34b27a1de30acd718c646920bfcf6d049d530d34ccc790c9b992d0b2ea359d80ff9080d8fabda5e138f06806c4c9ba6bba9c2a85338a9e44a7136dc53047b116df47daa9f746545a0a336c9327bb0039016366d13033cec8da6af8838779a32e2846ef69b702f72111317c07aa370341e6867783f7682b64eaa2e18a3551068808a3446a4ec9dcd7fb58d8f589d051543168659bf3018df9ae61d1fd83dbe624c0c52c3ee07260e04cc3c2b57b0f96e9fec4a5c6a78f67c3560ac2bdbddca9d06e7e200231be9ca5942a5a81c3269a2584fc7097f7a0be9dfeb116a0077807d454a51182e1a5cc174c0b5b04f149e48ab7206b91841cf48c70af10525fba08ea85e572845375761f1d1ca103471577e5dae560a679a6780fd2d23b14f92079e06217b6c592ddc2df91ad61d48c6ecb1c3d75a0c325be583718e4136a70579adf37c09eb84d79096cd91dbd82a667032842212b43255dfa3f88878b17d0969800bc082384c143adf588e703f6bb569352501d612be56131d106dd1615827d66918a6b033326d58f1abdf2e4988c4daf0d1ac0b0f5eaa186eb29041801a38c5c0235d3a50bcc6c0769a5b71891294fdb1ea0708e3b4a835f78289f218a284dfd748827523706dd94b4dd98b1ca27becfd255191eb7aacc83d23c0af84207b079474d36b97736f334b1e1bb570f7f374c1db350288b3669be02d5d36b938f8bfd10b12e96b1993a8d7377719179bdb997857f2bb9ecc48c4026721142f066191a4dfeca42a32d2c925565d8e875135802d2331ecaff40a4b9530d7d6fd741386848afdfbf2b3c301cfc0f76c439e5c1bd9208f74b48d2d5152755566ad8e71d16350fa8b0fe50b6ec24e1df20eb086ec37b0b1f629386832c3fc3f9ef9e48332012b0baf74aa5d3550361cc4f2794a5638831f0dd73f72e3bc376a29c48b841f12feffc0d22037511de2b76a47aa3aa838ebe653dd16368c3c4c65a84a45dc8b329f36d95eff0775402308d9d13c55f087c4440018c85283e053a75c4ad74dfe3b64204c284ad9c8aeae4359c19e6955a986ce08b03335d77adda101b23953f63cb11ea1b007a13d96c8123e8c0bf5d75b181368cd99d38e2a2e9fa1de90812eed6350bd4a82e73ab7b811712601db6f653f9f3c6be85ae0183c59a30ec886ec0967693639f3659dd9bbaed22f47f63aa4f4d8d23bb410d9835b999612beceb6f5653d70887e2a94bba34568940b5b19741602104ddcfc096de059f75e1ac09feb0e3c7897958d46b6851cf7f6872f833ec89b57e1077caa1dd1ebe79d06783b1b015cc7fc0e2ace54e77ec400fc1c922ea0a160c0d9db38801c211e4b11a010020926a32010250020a9d010a29063d98013fe93bc1f808b61adb2e0c8da154f9b51a7bc53407a30d1b97e948056b0000000000000000126528b52ffd070007814a29a502007403666c6f7753657276696365548485d8c08272460778200900571044a17c806a1a15ca038398f61877eebe2d402d64a868825aa9d3597d0a29063da1d7ac949f43309989c77e788f549396c3ea00aba7ba5596cc22966f72b3a3742199e83c780a29063e951449ded1b7edda65f4d58f5651c589e30d59bc414aba4653692d94b9242f0012407d010074010720d018020092096aa6707519d9a0a20102655a5019f4bb55050064014002575881750d00328779bca7178d03c441aa549e1a180c89392ee760ce619201610a29063f5987f87f58d7879ad2efe699d636636557c564b9765e2222be34770cd41ca800421229c50000408a0400338210a04f0650142a1b547c405696ec640a29064009cf04fda204b4bc6cc9990424a0212ac7aaabbc24b43db04304c646c40c53004005009f8715007639bb50b0185ffef059fd010a29064506a6a5865bab0fe90e4fabd61a9496830ec5aae6ac4eb2510e78414641d65c000012c49d05001409912d5440f7e3769e680c704e204e52a51ad5c06048cc713907730e930c258d120073faa05e0a29064cd8b65cc9569fdf72c57be571b00789057bafe4edf958220288e9e13982afa572157877737ce050319831a8688097661397640a29064cf0742a8f33de55fed5da6d96df801b49b22cb272d8d062ffc480e668aeca59001a631e88ae7f1d7c508873c9533ea84daa9985b853e57d9ae7587463d2efdc270057a0800b109d41431d94d9fcc18c0d408998f40fae988bfadd730a29064ea5b980fc141bb50def02a455467fc047093a65d3c6a4aa12d19da0a6d8954b840191200f0d500b192a9a2029c1073e52969502cfe3082c4cd435304305d0758270a13b894dac3782dfa94b951eb84d8016c60ea7c9dee640fbdd6778ddcf0720d3e9b735e38c88d10142e9962b8791ece73bcdbdfec81e6d8802933b321580335fec879e12dc65ff58bd6c7b418453705d509351d9a0e2030213da9cb090018bf1ca57a4027b0ec002b4033c487d07b40354936138c491b05c0ded4c6e15a9473483fb2b4569f936ea4420f6a210f5a9c0380400816507d02783abcb12db46ad85577e8e517c3d8ea970a0274a020c16347e12995c0300714ed415f1319c02851898684840c906151f10f5703e7fa68104946c50f101015475e149a7a278d20ab388a7a2a9a33aef87c893a9a3aba2aba25d0a290658f5a85a44530f95df6a18f41d30a376cab40bc5909f6ff2de3bc4e75f560a8f4e1225a5000038030081e50ef4fd3106150d10311580f27f0a2906598691820acff308371b1a6d0f40641f30228712060b951e9255f8ed3d945349001247010014001b00038d7ea4c1ec2006004f834eaa79e3e060424d33403f64a8688247ab7d430d7c50b4818029afdef61a0479fca71606007ba34e9e6a8639389850d30cd00f192a9a207716fa522677a899ac0600a1e6c953cd300707136a9a01fa2143451304319c6a093275548c4106003d8793a79a610e0e26d43403f443868a260885091996421473007ed700ef36aa2e424e70ac716c60636f6fa95b6e586401c0840690825b686c04569720615798d37469b0492c80edf66ffd8004675c3bc18cf2f8e77e808c65082e5753071dd78c9862b420ec4372ff2c98a51460601382fcfea70aa58001b25e0c061760c4ae320eb2ba5d0a29065b314a7d7cbee4dde23ff75de6639f4acf92175a48f6693c8e51e487e13df7d30032d8e8cc18543440707dce5f620a29065f9be26df3d104b7798881e718f1776f5b2199ae0b0a929ac39a0341dbe5d739b27ca9910d2a1a204820536261f20c0a29066168e70e37f3e191ccd22ff94e2d266bc50d5f9b1f078c866e773b9ecd6e7f85000012b90c56133d310052fdaf39506b93009f180500000014454481980cb737917f02322569414a227d4aae2a45aac693e0415cafd74348d6a668b7902bf992aca194ae90cf80ce87f361db0e04baaa29043d918aa1ab9a7aff3c6cf194d189a0c84e04457ed64147d99ce381a4c5169acad8ae9eada9c58eab6edbb63f3f655c953b1d7455bb902e2b7a1e039562f28af31283935122712c2c6ca1c294f1c223e39c4e234f6df4e7d3c90fd3193ee33093f32fb99ed333f60f4ae001530d792e9bb7ec5246069d67467e3ea3ba2a075d0555fc001fcd1f58d77e6ed6b119b14422bf51cb0aa39b0b1c8954a0f95f34f15d280baf58e9a351d9a8751a2852f13f79ff18d3c7e2bf8fe8b158fc5fa0e97d7a72cbaee64ead44f2895ded6dd755d2353d9de1298ddbdf530c088127298c40e0610eaa650b8b2acae941604ae60c5e7ebab46640d333e4b597ce0088264a1925c8b3c740b66df1f48cfd63ee49178406233c475cb77d03db56d0dedde433a84fa53d46ea3e3c083e3c0a5438a07b673ac92f74a6305794d41a5e93d7cd68bd4924f3335a1df7e6c1786a1dd71b26919812d70f9067dfa512366b550ca048e5794627b48c29745ee7677462781842ed7561ca32a6bef0c073c48d7d974e5fb00e3ac36b82e6ddbae724b594999aee4997e506d5b69a5aa781ea63704fdd3ae8e0e3c5a8c0140f53a68c134637a7abe42d3fa39457a9f5bc7a8adee99e4f6d5873a8974c6a66adfe5eec1d775cb763a452ccffe4afca487b879b7c61892413ab67c838e7656b17a6c8a84f7b6b1732ea25064462d2e12e114cf92f9ac3a77db55911fe0b8c6e994ea872c30b9fb9a5cbb26dc7f1b02d5d96e34c1a44043181507d44147180511ca332aec62b2613119f98220989030dc240c5fc2f14138a885528844434b568f2096742a288261329a2a24d261391567444aea29cec95d1aa5c070114d8c599cbc86b68e2bb406f914d00b00180a48143008d0c018161a0e19d42243302080800020400210401439124b7f40c1a5302f9959dbef63f22ec404a457aba9f67b53f756eb10500b052a0f39142492fcd4652d9e6a83d997c886a83c8f0c3800f47468614d09e0f814e528b8113e93ad63c91605724b42bb9c783f9409023a00200047f843c02024be443b026e95e1b8eba5f0ee28cdb2dd7b6e4d31b2759d3db1486896e23d6a038e0b0204f0471d9d243241263518c6710d39d081bde78259e9541963c301f275f1f8205f61bdcadcfa08f5f2db9ab1d082feb9c8434ce6ddb351848172334bd50963ecd8c67a57f3080981da097c7005c852d1eab11279abd5099e5ef109eb59b35f0c55339e08bb031f202edcb81306e62b05ee7b2013c47f80e5910e7eda081310e31c63a4b3c95db73031176363f2b968f4519f4987a828cccc73620948924afbf9f8801561041ef2a6aa7e32064f0a15ccf420a26470ba611e10c11dea0353907e85e0e8a4699f0810d61ecd4c85e5fc28b9c000f7d4988e4b991980c44393f8f127f8c93d011b08f385f2251432e4443225df3c4a2f4982c07d971f24271668844ef64ce72257b145c9f11ff1a62aae79e8f481e4ace1ff44b138432f5b8e453254eabec7b42f9dc23e282f2b444075fba43b0a4979c58db7a30cf4b60a3ad4d05a74127420878b6987b1e9e1a4217a78645206603dd4b80c1f2ae30cfa16358f7b2d1f224c946bce0c37600823735284f6f6b28d9c8952f022172341d05d88de1e08b292f4030a62e6364e421d18ba17236b95cd001e7b36534eed74393c34c2dfdacfe56b49929d5e8a945e4012784c2d24ba2bf52bc182557b3f8ba88a541fafbf9ecab08e1728d8d19421c3125bb2e427c82454f289cbe5b4dcc5fa28c80e8833c7d7446a328c74c196f7a106d4c5bab6ca013be7b5faa9b72d4b9e8eac060e0864797d366cb47072452d36341327099ca098f4c77eb75207b1db84c710597a2b3d445d6ec6b1a26588b9c91007eea5f6ea5221c4c5567579860d677163c2d495045cadc4b2553d0a5e6e0c5b324b408db99309716d2b844645e362857ee6c7f428fb66253530016ee57d2dc32c47da11f903d1c473648c3e4d8920e52d003fec6c56974f37c6b83b7130f14dee09527a9b052e856038f29de1baac8e870f8e79d27eb5e8783902d0400633869fe62c97229e07a054404b82c24b4c8100c710f7afca6474d8261ba43a03f40c5e20d966fa1d91a010020a5793201027d0a290663f1e58889d61f4ab162ff6d994d14c5ee93c5b7ea9a43b604f9476cea04df31a5b2410d1a540c660c2a1a209b7a52825e0a2906687c1bbc949c4e0c6562d5a73a4eadf918616f7b0bc89ece90090cefb3ab8432b0d19968f2ff2a600a2906696f99a61f43cca695f548fafd57b8926a0a1999fed0d4bd7f023306661dd0f11a38429c65838a0608f1e359fe5d0a29066adeb5d385dd2403df6749999eaf98fb15a9cd805338311a4b8249eef19ba29b0d4cfa8731a86880dd44f28ace010a29066b36f3c2abc7800e21b14f9cb98cbb495f7cdc20e1c077323e7b9774b2229e7c00001295250400e40601400252636f6e74726163741f58428485d8c08269f603826b746f74616c537570706c79d8bcc680000700498bd084f22b35021812735ccec19cc324039134932b970d125f750200540352546f6b65422e33a00945c160e6049174193b58c16298858a2608dd971a262c32363a4243474e505255585b5d5f616466686a6c6d6f71737475787a7b7d7f808283858687898c8d8e8f909192949697989a9b9c9d9fa0a1a2a4a598a69798a79798a99798aaaa87aba1ab8eaca8ac95adafad9caeb6aea3b0c1b0aeb2cdb2bab3d4b3c1b4dbb4c8b5e2b5cf640a29066cf341591e7072e4aa234a14f1e94a8f6debbf75394c93fff4dfcb62fdcbd72cdd000048d0071deef356b9600a29066d0dd126b0559a061daad2dfa1cd9fafcc9bee1f3a4b7586a012f969d04a51ae0074cfd6d0961e79a9050a2906717f268d02e2c4dba41633544441dba55e299124850dd54633475b845d31b8f00012f0049d04f51200f21b553380074d00cfb6cff33ccfdb4df2ce7eefe4f3ca4c622fb9e925856dd04f154f90670b845b1142129e95cd450a00f42f84285201ebb11effbf12c7d817b22f407110a6962e0972c5f94a1cf42431b425bef73b534b1fc5fd5f71fc9e7b791638638eae8dd61a48bff814e771d6e6a938677e24399f589deca59584b071c6189bb277367df14951e3dc669594e7e3499c48723ac5591607992e0be716172ef4007415a719feffc39386751b16c617929c0e89d52dc61773a9270e324528e5d636cad72d56f7f62054a7f7526f225850ed8c1cfdaa9528340eef51f045c72aa5450c503e54a78675a933bbb3a32ffb11f7a372e2e83cf7347203d98feca81e71f793f8f4bdd7d1efc814c319fbffb51e4e999f1faad35a8926c134c50e50605cc4348ca8245c0828c1340d444011350d8259850373494ef7f79493f4160030e00ab78129f35349f82c1cc97620818210819140791065007f4400c9cd1865d2dcb4cc7d3c80026f880c4120d14c83564200c88e9528f6b5f3aeb8deda6603eb8d2022010d02003553055770258a9d2238809909569911b34e555aa430e0ad01ac8aeda8a2819278a0b51dacd6067232800b052a0981a995814da909da0b934be06706402e05140802b5bdd63403582d133102fa570541e201af1a22716c7added32d84609bc5bb0a999269b7d5147649101db6507536d0dcbc0bb79c9d6940a3a64e69daa5d540eb05e32f8b11a987dc78c719deb3ddc09320babb841a40f789bd5b57bce4646c605409315584095383e5c185bc6f40021684beac3884960e2f459421fb084f68cbd8da933824a0cf3703218246707faa0231b5434404b1a49e2730a290672d8c176f4932c91a7e872e14faeb41f4bc53f550286b60aa08527fe56d612a05b74015416be0d2a1a2075459687c38573a4093dc3c01ec48a96426b345188b651b746252207087489940f9d00a4aa824225a0285436a8f880eed12a988655018a4265838a0f087cf822548658a2ab7e71695da1ce5dbb856c6c7a7a857d7dee899f1c4e1586899f96140800e3189b5cd50c1f7030e038d4140dfae40045a1866edf3086a2249d39d423ada29d87b08687b224868375f63d10a20fa6362914104d231e037de79a21d39ab3f4e03d6c2bc44ef9031aa51402750745ab62300323bc028384b32459b42a063306150d103e820f83730a2906767128d73a9cf9651b8e9ffada7ce69a7aea48e57b3cc2eaab100909c0caca5f008c840181205ad8600a2906784b8763a61b5793f81449b4b6dfa49120bf6a93c06d55bae47c145157c81f17004203280a950d2a3e20d3369e69b378cb593c89c2018183699e321d150723bcde301b696cc4f48f57e8a330061e2274bcd7daeea37f85094e543d91523dd4473656df991e9322ef285152060fa0c7aad8104e62e5ca39bd22afd98f245ccdf3edd97416d400385cce90a912a5ab746572f86c4aaed408850128d19c1d970240eb853ea50318d82c2d630a29067a4f5824c07ea3af6ca554492c85d2541d332b84913a7841b7e265a65c2685ac004ed0f795be56fa600a29067be9fe5e2f28c65080d998e72c2c92c373f0fb67b2eb57206c953905e372b1e200080ce8830e6e1a8dfe7dad0b2381edda04f8be02a043000f07e08896e89c2b7782c04d199beaa2b8488c505d80fec150d104015419419291010a29067f346f18f71d5210395f69fd06515385ce4897844073c991d814ed1498f2ec9d00001259450200040324827246416f746f726167654665657307006c8aa60427175fc6c88181733a0bfd83a1a209023986c620917d0a290680b1a6c892db10c248a3e347a4a95c2efa05d8029f1a6bc003c81a8063a247df5b16f6a4f43b83cdefca526445537f0c4da18b51f46aa0932f406cfe53f70408f9656b519c0980d4649431a8f88046c2fbbeb28664f8e62e912e06eb60a7bddeb5dc200ff7cd3ef687cd0e08c82d8917e484851a23a6e8d2e3f76e47deabc25898309a9831bd79a8c7f8a4f76ff233f2c0a9a05db2df7d282079658ebb37a382d25497fc77eefc764a9cda3c24b8f65b2b6fb6337228c4b933a86880db40d7d43a01d2b05538520264e6cdbc610357e1d7c06d040e7da4be7f05a0c8b2588f06e212453c9f07dffc8d2ab3ad084d49d330b2bc8892b874a102c0a44557f2f9d456444e701a374f73de556a4b5b172329cccb3e001283950300e4045818536ff60384781f734d65676142797465735065725265736572766564464c4f57d8bc1a3b9aca0078196d696e696d756d6174696f6e0b00603040dd2157a323c00db97c35c0a75d505168a9917d5dc607865a0cf16b605f0a29068a43563396a930e5521aaa0bee3b3acad0525e7474ae6b19f5a564577fd9edc640509351c6a0e2030216fd685d0a29068f28f2dc4ca650a8150b02fde4955626fa3ef0e9b22cf5b2d37c198ea5322200400300816507a01f3254344186b1c1d07d0a2906912b174fd0a284a105f8e299ecd896fd53e15985245be61ce2607ed052713dd1326c74590b5a0b610a290693495fa00704327a2a5df324e49ddeccef8a3bcdf081566bf47c5de0a2032b9900749fadbc31863f5e0a2906934c351d98a15a781d4d0297ab2bcd71644f9517a1e16e0dafe49fa5efb6288c48106992e46bb393da5932a010af9f059685be7d9d6093d760394f39f53cbd5d9e5d17dfcd00a568309ab3a08d6b2959cf8cdadebbadb9f2deb628c20d58b3bce9e509e5ea879cbca98acb1f9e9254cca89b3721790ae9135918ffd6d8c7873e666acef34295ac2a60acd0cea0e203024a3d9f86897501dc118c3182028a5503b59403cd998414a104a4b103cbb4b005798d82f8b3a3963c8d7d6c8428c85cbe44735178788e453ad4ef28a1b6e944f3489dd5d6c11100126bd502008403514665586e68f60382657661756c74020068466c6f77466565730a006383a4d8bcaff32c01e7330329112608a0bfb8a4cbd871fba0c3185434402e4fb6360d1a2632424e5b6874808c98a5a4b2a3b25e0a2906992576791c0b707e0ce17055e6ec3b2d3705b5ef10f42403c678aba3e43f39b1008038030081e50ea8c9286350f101017d903d7c0a29069d2cfeac91015a30a4832e2d9c63e80c328ee0b3821f03c3863a31f7b509bc0012449d010004024a8168466565733ee8497a015b610a29069de471eae528bdb95409eb888eebb77a20b0fa3b38e249b9cc44480eaffede0a001a898538cb06150d10d5ca99c6730a29069e56411f0da770b9192a225f0d7b8728af0f81c02918c67ed5f25893ab2ba8f400267401ccf68985cce37d0a29069fefd6d44f25876d356e29fac31cff2916dda5623bf4f5ec73e7fd2c762a6c92000d26fde97ed2c8dd85a201ad992eb7849378fe16f4d2b9369e5bb41d829240c28da17de4670bb639f2683c182b9f59906f867581b1186590a18214c058bb736ffb82148f248f8591919494a1a187ae8687b01486740a2906a233822b23d291efb4304e13fcb41ce0b251170edc78e676f4e10d2377f8f2ea00123c5d01000401480c08003987094835730c27a24000e02ed42c8682c53006150d105ce5e4f8640a2906a2d5d9b14650f88d80861b8e0e0eef30187b6d81f2767d30abc9ee1ed1d76ed674f0d99acea0a20102864f7dc75d0a2906a48aa30b923c38d639b67097573aefe13ce503527fe1756569904e4b438e42c5008c0abeb1b57d0a2906a4f316c1b4c76bfa40606cde89bed456fba4d97a86fbbb5a2c94c54d8b21dae700983241cd148cc315cf5d0a2906a67b6468612000e8e02d8a233edf9617f8c134fb6730098902a67ae5afdd451400a538185746da5e7d0a2906a6942a208664074670c15ab6b075b47e36326dd312a70004b3bdde1dbba5ac8e001a0a71960d2a1a2044cd2defa6bc19b3a0556e31bd692f91db7936065747fa657da9c6fdc4f8f0384fe279238c64055ee27c7f53074d57e04db31b417631914d040c9b3b00c880f23505099c0d297fc0f61b19896065f92ef66da2a7a2abb16f23297272beac2ae74db1ad74e6cccf2e40ff60a86882e75e339b01fce65da085bdaca3882ce421004000630000000060090b8318dc81611405024196770074049612e82b36694f48a00a377d748dd7217932f9817f79de2bbd2f7e22c62a570d77810f28083b9f302fea274d0810ff77aa1d659cb98248b6a6678c8d2552d703e39557878c7f43b4af1ad48a0a124bb5b0cc66e4a0cac4eaa28d6f1f1a150c44b8fb4af5bc09832c3c15bce38b246428bb14730ada7ebe7a30846b12b74da2b69e7a65e558d128158a2777511b26dc13b628baadb060009b1b29205e838ea0bcf52b61318cf2924d7a9b1bc990eaa487469b835037c911c98011650df6201fa683918009b300a890893345ab1e0747055a8fb090c43094273103a135832c90a6011c4ea87fbddadefa4696519fda2aaea2ec11f1efd9da652420fe5fae616bbf90cc885b11ffbd070e3bc0e37fb48973c8b845918751a5e1058218b23617375376d05c03094f4026809b4ea90851c1c49234aa5e330c1a00b0ae373e347ef1680331833b8ef2983ad74157aa4173cd381f847490e785f3397b0b8b692188932998b2248def5164e0f11f6f187a6356187d449e01ac8d2bbb5fadc6d408525b8d9f49537ab55f71c3e6e833618d18751cbf76e84a4301187a30cbac41335dc6090bd252e4c0a25b38a914c6ed9b94366f7f12ebae16f92ab6316e57cb2e6ecf81fa06d7fa70ceab4a42d30d677356b96e8ec8249bf000cbbbd50c66c137c1dc5270d8bc13b36d62010ccebd5247a264130985285c4bf3db269bf0bd8673ab948e806d82cfa0e0b0b9ba8db6d1a66d70ee8b7ba8910da6545338ad53b4996ce4391b9cbbe2c945679bd4c16dbc827f34358d865d9bad3992bc0e94991f2a3a42fa1aadd29a74fa204955606c6d10894a93084104352b1505b2ddcb184477704d5fd9126b599bea01d78f91ee2b197e34b9a112e2f03160bdc2f2bfc89bab012a9e09a76eca5dd260520be09f3e849ae3776ba960d591d5a135a2a8553dedd0659084f476b882c247196e77545568674593a0724f938f1e17f385f232d1610a764e70060578999ed8361e039bca95787cdc900cc30ec68d441a12dae37f40cadb41246cfa50ccdf6131720bc72b912300479c48a8e71c0fef9830c5311c500a4d38d9b95216f242a3ccbaf6bf174bd3911f3e1effeb19af611046f3cb2bb994ab79ca7717d9a1819de7a962c728c1a85244c236df3080e6a911aad2748ca8f0f8032ebbd701557f601df28710c4c43350ad92f53f93db8f21857bde025578e616e1bb18c4c26a675db569ce98b71c03f61dcc812a7bb43cebab1c62ed1d7eec25dc8244c0df82d702c0ba2a615cb03f4f539066cb934cfbcc27f8a272b04b47f61bc534775691a0c07d10ebbcd5e0d7ea2afa02044df1c87b9f848b68132c574fd674da9284a7cde6b355e6e2ab65f47eefdc6689862f9b62054719029be4b2eec049008b60532e2b00444d6d16bbab3b344143aadc95d66b86da01588bda230cec9939d930593dbe8ddc205b61ef83dc776d482b08cdf88a599829b33b065178f6a1afb6bd07d6267a80fb6742ecab1c0cfe25af0dd1ab227106a6b94cafe7eda2af401573bc2ca774ab696fbb8452c87682e066a42ab5fd9b191c41487ead34a688aad3edfb8e206fddae7d1c769e259f5bc59b29a0544aa7b79f01c4553fab552c41fc302b686d0239d42b39e9c13de8c053bd5cf3a649a8bd00360b24c85290b099dc2014c94288f0733deb5c1c43a4a98660732365c41e46e214e24984e024e28c067c8f2db4123c738f183da234c30ab52cc9d7a930ca4fae4a12a38fe915cfa7249a5a2e595365cd0ce3ff8b3d94e56c2f9e0583094dc2f441b043090d9434050b9d811d2285a5211b6640e405076707ed45e0200933a0472c0f644ee4e03f14a047651e779122b2ef5f28919312ce8c1baa9512d2c6fed7ae7b41c62dca90c29a1abc40dc506bdc22801dcc494c43ba1713849997807832f8a4f82630a5827f2366153f96052a81c894926c4f4d3e4c98847a75bb9467da86948ddceee7dab3683ca76410645a2c5779b31036e64fba01ee3bbe28ca2e45f991905efe83102c8efe07f5dbc749318b8973121b60172b13129ee1246e854da802483ccb49dc429b509590f80c26612b6ca20a21f1594ec216dac4aa8084673089adb489550809e987c3f67b0f262a111d9102763e46e1d9877cce10f189e8c8148ef3310acf3a64df0c179f888e40c1361f1b9e12c1e0142dd89a2fc53bcf4bcfec2379dc309fbcb8bb5cd5d8033c579345548e6fa73fbc43bb8cf0a11476c98319ada3122b2fa66ef250ffd265c4a661a2c31ee4f65c4940f96973db43bb97c3881f4b28873c9209392aa1da42e3240ff8252e236e0e2f5cf60897e04a02aadea3d21edb820a237eaee04ef248c276aa84aa8b70933ce0cfb68cb82dd638d8e3acc52109287d12477b78713819e143c9d6920733af462556beb7e4e4619f2633e2f670a6610f72258d24a2fc5cb4eda13b42cb884f4b7d863c98b1b32aa1f262289247fa7f82119b86c10c7b909b652501e5a7946d0fed960c237e2c1119f248a6c7a8846a3b15913ce042918cb8396222d9230c122909a8fe43447b60fd2119f1db721590477220a312aa2dfa4bf2b8bfbe30c2a6b07b610f73cfab24a0f004afedc15d7661c4cf25e94a1e9a59ae2a51e982b8240ffa792b236c0ed756f610d7b548228a9f686f0fdc959d11be96322b792823595462c5c562491ef10f1646d816e62bec016e5d9544949f6eb53db23bab8cf0b50456c80399acaa12ab2da44af2b85fa930c226a13db34771f02c89283d6b4a7b7ce75218e153e9a4908733205525545864d4e4417f4461c4cd6185ca1eca1da89240e9939fb40777f594113e2bd2863c24171b95a872014e9207fdd894113687a3297b88ab4c2411c5e730698fec7809237c2bd512f240c695aac4ca5714491ee68908236c0a6312f60037924a22ca4f45da1ed90d5246f8aa100cf2c07c2f5442e5851b4d1ef19b5146d81afe22ec012e169544d4fe95680fbaf1c8889f4b43843c98e1212ab1f245109387f87bc0885b1fcb640f93272109a8fbb3751b9d3affc711dda860d6dc5cdc1683bf21b5b564b44729a95c789891da970c224138bdbe3b97e24540b696a47fca87bbe173d51d636b0709e8263b7b9e414c11f0be808ff64f4e94d28381447368a4a7548c2dddaa131763f6952360028934cdca7d6b31dac2b863f7b3160bdcdfda9513d2e6443374d014141c0d3b9082f50eb4dac679965366b6ca403a48726c13cf69e1db57d9ad1491d6777a1ad3c8c7bdfa9058f9ab38cae8d7da3488ca827cb1b1283344e79487ffd086c53511bc02ef333930a1b072f9122459441ac41b4d7a1608ac264a057dcc9a9b00356385582702e48456932117313ccd4c80aa47652c552082d894fc50ca341b350183031d1ebb144160da4d05d04cc0143005a995a3f6e0a821e2389e3cf9239bb798139ba8c5c5040944e7c4496835188acd0d1bc8a5d146c4add5cee3d13d33c9c4ed0221953014612eff184f036a4860b08537e447ad41c05f94e47db134e6989555e594846da4962800563825300946c7146e031eb514c15e9b6f7c79eeff98794b68db0f62fd42074508fd2b2cd61d6d166bd548c481c6b29f8799fd04fb22cf2c13c9c6dfb8cd8a275cdc92183f1dfae762160b090a716367df03d2482a7654b388cb4db21b987d17108e523083350b7f5ae23b374a1959a1052517c8fb7fb713f1d4b805cb23eb612b34032fe22c8690aebeb06353326721227e82308bc7d3d095b4c48785bb8687646208f41070c42cc40be8daebc2488373c8dccdf3554381eb88c82f188f9ac6b9c22c3208f828012e8f11d257d81b08734325c0c5db5dd4ab5c6ec895ac9b7de0db91758379fa30157a67cd391c06c7ce603b4140c76e3f0cf2141a3b9c32d3f756462aa364a3292ab44ff11b29edcafc9855c4b5d9a66c871bdd6bb55000306e1ff384040fd2e353b66731e60868a69a6f6e6c875f1b647b0c8b4705a72706987d4c3c254d8e04566539636cadb4ea0289fcada074a18bf0fe5635a349f7eaca15364d590b5798b87656b1aa7e05b8b32ef50b9aa8989ad87dbc8ce6d214402c3e152b0633e8cbec1f144edd30c84328104e6879c3118a0d47dca3778b91f0cdf24761c07943a81b6375770a8ac9db437adbf37667422ac8fa627bc77aa1fc9df07f2f5313a658ac554696a3fc503cf320d056bd015549e37d1a5fabec45f8ac8d01fc4f02b3a90202894434b2780a2906a7d02819bb6b179015518864f45c944da7e2924eb903cfac17b2053613c67b803020f1c60b063528866d7120f301ba4d050054f37013047b4309912a95a77a0a2906a7f00bc6b32a0bd64d620b1ec353b3b27ae86153fde4f6bc2ad38a92eba5a80012428d0100840e071c1c0c380e358bb60f3a8c41450304fc314335a9f21a488636bb92b1dc0de3ecabb725d2865c95fc774d7bd9a509ee133750d45b140233b29f6c829e6a2459b42a0bad0f6f8b836a246ca2f358eac16da384c2eac66fa43af5291b71a3bc71a97f7ff980a5808a347080a583a48390a590a591a4fc91e99f9fa4a6815c2179a4a6a5a7b379a5d8a7a683640a2906ac55d8f29aa31ad4fa5299b8a71073c4bb1c37d2191841be63181f808bea211e005bbba0b2b047f8618b760a2906ac8421bf98eb072b43e7ee30668db143f713829c5cb1295e053aa8832be14a6f00744121ce2725bb63640a2906ae412a9dc14de6822360c97effdc2a99f53651f8ec75ea891b41f2a0e96deee5707599cefff50aa182010a2906b00ff6237aba0c3fd78ba4d2f1c138bd233313f4c2ff4f1bd06a36c6ae8026d70026124acd01001402196bb6c7185434403cfd9ac73624667ba0c2ee758382434347e14783552c5ab33dc6a0a20102b43429836124618364641471247373809d68204d8b9d8d9cbefe30f69b9d9b9d9c149c149d249d2484aa148384ab248384ac14a8ac95760a2906b095deed1ab2b2041e56cef12834544d6e6f17cbd3cff5340d2d7d3f77a9e1610074c1676b64838a0608f73a45b9640a2906b14cc87aef886a6ce6dc56a307a43e2c4d76ecce52117e3e224b23746228c30d00502a129d905d0a2906b1e1728de779595c787bf6b2a0737660054e10e41f154b396ce19378b87b420f005b2ce246250d730a2906b291e10a56911eb230337f84fd9b53ff6f3c48dee3e6c77e1d7ddd3ca2150908a5540c080fd609760a2906b29477f18e0c35f519e7cd3d6d2da266b2fda4d16dc6afd2cbb7c196f16486c80074010928d9a0e20302bf3a1fc6b29feb86b3a7ccb3e75c4c144d6a52763f35f5b665815107a759389a61a2c49600f441fcc13e48a589010a2906b5c30690c0ef6ea950ed106ac6917209a7ec9241ea59d6df5e754e0a0322fc9200001251050200244102827709004408e0997d390c04933da36038688f71116a7a00f2da567cb2f5e73e58df5500bd27ba1933313d2211b065efccbc87e35e3728879251fabd7a4d03006405b60899344f1779bb4c6df5a81db73e5781d895df06dee951e813eba99e76dd3c9288cceb5a5a9ada390671f60b71f3fd2653ca5c4e1ccc6f8b5a62be6d17256a0201aa0248950d019176c012dd0d011a02b3cfa4b12603f2b39a1b3204780e955142053f05058d4e0685a5687c5b07da547035680811b8b2c37409d7665321800abcea84d88c0ba06bb935980c86e7dccea50d42118d77b20e49100b11b2600a2906b915fe002438be1a57f73840ceb4afdc8637ef4261560ab6140ede0fa3a3e8309838125013892fc5a7b3bb71be93f89167ef4b97104fd1032757526208cb18d8c7af5012c8669b692fc00d80fe81d0697b154ea67dce1a0b7f8c0e1b1c7005ebe054ca0ed045892ed6bc9640e8e78aa61ae72a0a4d792276f43cec4a44d3770d5b3e1c1c7842213f0b053a5d98f48f64234a3ab32c0101e46f4343020bb7844558038d9623dc6604f479541a78057fdd48728e06a6ae45289c0798c2eaedb4aa082dc8398eb35e0a2906bf95188d693098b0efe0a85c68fb32d6866f0d3ae12a0397eb46af656a6bfe5b40594171d9cd7d0a2906c38a92028b8de05c35c61e517ce7377ed0c49f6bec91d7ff3d2b49c9dd9b0326b2410d1accf61116f34c770a2906c3e1c353edeb69a46572d7c59d02d257a04ea9bae595d0440ee62b6e37bf062400745f6afe359087767b0a2906c57a2a2275ee432b9ef783f873af8de2641dcaf40859c1327730a05e1a85f87b8cfc118cb382130a2906c5a4c6a1ee09538b89a9fa96e69cc16da3028a5a60929f118e31ff5c72e1ebcd0012c912c91bbd49005a5f680f32604d4800fffcf3cfebfd16b390e4cbe49d5af25e6e9ee4973e25298bb05ed15b031b003e2d2eac3e4eda1eb8a3f4992ce304e700e000ea00d75dfd5282c827d3a8d372cde99ab7e9ae5e4a372148d79e0e594a75e64c8a0c253c1a8fc71b0f976a39eb303086e98ac925d3eb52237b56cb7436676420e8b659d755ba6dfeb2289362b3ae2bb246f68d6f53c233092b2094c9bde5747f347bc8c1c1b99695b2712388ae06f54af19cecb927d391656c973b92e9b76693ac354b652a722b997e23927afcb3aca5faf6b915236797fc8e5f2e9063fb8ddb45377dfbe92073c5de46cefde58ce11b997e32180865c2c565ebfa1f8f1320809072a8149baf99b1187f6499852783710208193090a8011ef231e4908e549b320010b03028b10420119adfbecbb7775eef0b4726cde60522afcba5d23c3dc3defe7f84445c79cd9124094a9204a1c400420e970b2bbaec64add9f4e502baefc5db5235eb9647d0a987b566316a287efb8b71473b6cf82e212ad919bad64c47d409b70fd1e520f37edb70990dc1360297d99cd2e42b26b46c8395224417bf3f121072eca35c652a9de9e1727c23596b1044dead05e460a657e40cd1c5f4f9ed72e87cd5b71b35dbfcd233039b6586ef0b841c9c6e9bbb7dea5e1762c5722dbc0d44fe61ba49d3e592ee88d221c3dffed38b2d055df2e37416e4bd9efc30a5f26c74d8929f0eff247d4b2b3b1c8d402326149f0db2f2e6d79ce1c4ffbf41833bfe3f1f43e70ba77bfcbf0808b7a8d0b75c798d31640ccf40de3aa69f4c33674a4f1cd9e42cd78e8fd28a0ef2079b3b56fac352934c59d78eebbc987e3fe58db498f2da799da593eccc5920190c679b74acee752774ad3cbee31ad9872d87cc9952577183e85a76316f6b7b4e079335d225a33caebb7ace55ef8bf64074b95dce6ebd0fe99ebb686e6febbaa2f080399d52116ed1687ddb200546b443c5a2505e6b4c97cec944aeb27b35795ee96492242a432c86ab975e2858006259d8700b0d0e9d2baf65a3528a2f253c3fd8fcb0d61ea77b1c23d088ff4723d241e66191d4278331718b048e0414685d86389e9179149f6e4d0595cde303d99e3e5356636b36f5aae284895b545450647b303a2117bc1ccdbaa5db6a90d75566bb648ef024d1b7f03a2b854992a093aea564fa02825750bc4103882ed50549e549928f74bba4f238ae948d1fc3f721eff6cac9348283742ab9c0e3c1412d67976b21d61de516b3ae255592207e5c3a79c56c743ddbe3128a78a361b1bd2616965c41d14b9743c7397ed9336c8c87754ae7365d6588c52b92daa8147561eba60a8af6f43a2bc5ccc0e64cc8da7b59eadb4af1edbd59acf13363b137565d31f53392919249c7a77a2c69824ea8327cd231866866460010400a52081a100c49044913be001200609918e94184984166400440800004212001000d4ab0f89bd95d9a2f65cab182f12f8d694f728e2697a9561ac1b75173cc42d774ed8e21fd84bd53c0a9cf9104bb13c8d933e998d5f4da05d52088a09ab8af4768f142161cd110d3190359af068bc0cb081e697de9e45c4e51ea7218cef738666881a2e51bca482624e27c9f98ace7651eb7e1a2d286e85d995d94b87e54f9e27522616b71335595448c7962820338df118249264235e8c622d2dbdf2222c401a454ad92b22f0331457f343b9352630f25f71dc272d108cd1b66a574f45b545e52685a8268750894c6543c22d6ec8f4b5c19273044290b077b2383f6827900214bc1fce494d644bdaeb9758fd90c557113db04e268132fe3d5f031181b9219bfcb382aff81c7153588e023040ebc776ed8a65c4717b4c18dc08c454885d142b2fece906d817c1803729e468f0e4ad405795d1f3f74dc94ce6a9ab52c0f87dc729a14d3bba4cb3540e871050dda5b89b6561d2f5857973b8fec581f86314d70e4c87e91891e38f15092b991c77ec4a261b8e3c3887962174114a20aad08f57d5a9d5b991160077ab12a693d8eafb7a48bfea8102cb457494a23031bf0670d7eb3210c45e84d9535cd86f624dab4d2a374fbd3757936746c2948a600a13cc38e503c52e0b6af4655818115659017463739551889ab6621f864e296d4e0e228d70cc957204777f4ec061e51c851d574cf91d2b371bcaac80f05b9c0113c0daeee9dc150ae7c1410c7a0492e45cf6c6e106d037a225fce48d846e59ebde6adcda2a4fb7e4f03a4072f296612ddec29c4428a18ba62882e06d5ba0e53f393821442ee66642e70ad1c06b55588b9b5825301eb5593754455042f66f448811b6e190952a8847cc7343eca67ddb6a540ba01ae104141714ab10c03a1baca14e5309289043981c9d6aa8de5fb450db138488859cbf82388782d63af8726c7566c8a22a14be0f34a71c00036c6e2ce90ee2c624356a389184b668a46542ce3e2799c8c16625f5928694901532a64419d9618e3808fc3bfed72359790a0f59a88061cf26b0355c367a52e19656a743216a8045621533723a6ce004c79397839e6633d25d9c99033a9741ee0cfa9235258766836db14f7ce3cc5d75d2888a3424e9805d4c7f8ba9b068816c8d4972b7303aa39d025ca0410857e542718ec2b9b801ce59e3ab96e0a53811af78012550fd1c039db1f5141d1e1f19f45ee9b44e55023dcdf0e40c01ddf0fec9734e6e4306314f52d3097b63cd26fff3495af5105d5db80287575b743f2bfe436a6efb8d0e4763c1e6fa82c38e5c3c0f24a7ba86411d0580d9dc22bd89f99b6574549107f11f063a09fa83b8066aa8f623f8b7c61cd3a18f72c867196a1a60e056b0815bd4d3e95cc912ec8393e80e880a89f8ddb1b8e20153d6ace9f2dd7ce78cf8adf38d4b7d7ee6d7350bdd27d8d5fcd14d9a3d0a412e5f93183742115ced1a9f0587fdc8d25b981e5f4b842535ff4767b4b3417a135bcf5078b77042d8f3e54865e66332531fcea84fcb8c697d56e851c22e575336ab8e2e99dc822a7e3e39213de13e5be2dec5784dbee3222b4868338d6fc57c598b2cb0129fa930674315d71b5e79901f6a5c2aac0f09bfc0e02737786421ce0df5b7a29250ae8342c3d6a610413403b52356c698a07d3c5aaddf4956774449b4a22a80f15acab68e3b758eed147767c077d4481a486e02d81750d8e99b3b5708e0917b8e8fdca14e233aa59634272aa324c580205363ae962886c161e5782c906795a18900e916c0277ce9fae35c274c1e994b33b952962cc6e646ae83773338d54dabb3e47574598881c764e05d51c6fdd274de4e8e9ecd7a3abc1c2be64d942881970ca1375757125b485f705158b1fe61a010020a579320102b1010a2906c74b539e0266dfe5472e34d8e95b8a8bd98eb9c391a645ef9888550b12d7512c000012794503002404581b4163588a72f603866e74466565d8bc007261437265611a000186a06f6f7273d8818281d88381f50c0066244c86e2f4e425a7900f1e5aaabf2b10a180e078f4d03875191f186a940bb782b17d0a2906c864df5795f437655248a0ec47128524fe7ac35309eddab8808873638c95259c00683241cd14e4348a937f0a2906c89121e9275386408cb8965fcaa08ac4edac66feef1e02a0d385cd16b6f2b7f4116c8649ffbce98fabdd802c1248bd010004029fcd66b0898f40803a8042a1b4346a2480438052805880669f6673737575787885858086a2675db6ab86a287a3b641249589a42537b9f78da54218ea898ea4e78ed49ca39c81aa80cb37456590cfa78aee5c49078b4920eac903446f090832240bf7e2f08d8f6a5742d1fac3c01347b8c111873413512f6f911e10b9c1170ba82b951177da46fb227f233fcfa170f6d14e9cb6f33d60a7373f59f7d00d114f714642bba5acb402129d807280a2c49353505d01e618b7736c0207ac934b7d0393ad2e0e8904071817459605d113096ba206882bbb66b4b20735d5c14db207cbc032a56595b199dbf8d438916ba82125c085f92d3d9a34a718157057ab53ba4e2bdbf71ea48bb682835d247d7f45b36119825fd95f6f9f28219a959f7b14dabe3fb806042a8387241c9e0e66879d961496979f97249a14b29a9f84a69d83a62484b4148384b524839a010a2906cdf02db0fbf95b2046a5355650af4dd97de3edfcda8b1aa5bf7b7e2f00d934c20012628d0200740355663f6846037608004210e226499aca0b86fbec31ce098460500b032a9a3522b47db0ce5893714fdd94fbcbe211a39e433ab964d2c22afb2cda3be611318c05de51653214021e6ba3332751db784224363abe44a60d422450145a1b9d19838a0608e33526c183501464246614666a78787a7a8686929ddcb9d1d9929da09ca5d9c3ae84ad248384ae9c835d0a2906d003f7a7470dac6efe157e1cdde437ebbb51ddbffa5f5f187ff764314951374a00142c8631a86880dc4cc4fd92d28550cdd6717984c8a8e378d1164a025608debea86f473de4fca169b66546a3687463c4c813ca630a2906d68b2df65fb78a4fafa2344e98e21b9e4826a47251f96772e5db09fdd483e47498e54002aa33a8f8800d4fccdfb2dbacb784bcc14d78c409d8b22f0b16fb725fd367823ff6c1bac5fd1a16d29ae7a53d2d2bf28603163a7dc96b5fbaa1e688cc01ff24fef4852151f01d1552d2fe8f9d3f386dbb946b2ca06371fd4d35a894a478f910869977d7e73446d63d86ca9e5d503155ebe9a5b30144a53889b2610a2906dbece60a46a8f1cc4124c2805ef0dd4c5b2c01372c0fcc7bc3738de87b6c54519824a064838a0f08d0791bba7d0a2906e4239ab2372406bf7b6eadb8ed27bcc66636611885d20c63f25e2a80875826f70042b714fdf25e0a2906e576b59a96757968374fed1088bc0497265cf169da39eaa1efb959be160d562630db4e1253e0e70e28dbf7c9409e30ac25723043504466b2701d877fbf33fbec55379ab33dab1af415073403fa645088574686ab942c0402a107448580bce236a20a4e60893aa13a47e14750a34e5064b950a352a2525fa3a361a2e461d16da16d7b7b7d7d7f7f8fa18f9f9fac82ad8283e806a45d79b9107c99abaaaae30963056c1ed1e6d550cec0000a07c00482a68480140251ab26a38c41c5070488ee0a2e8e246a32ca18547c4017cd347d8e249014f590e29da024cc77b7b1a2a1a3300eea04a2a4d0fbdaf3a4a3f2a4df84b283b2a484b4a3610a2906ebd21cbf6a1bf0213fbb24948fa7ee14dc4fef4935bb7350fab759637a901b6a00688c15606adb237d0a2906ecce0844649da203ea381d208a82f13eef95ea9801665242de2862afba3f078900f2638f06650a2906ede69cbd9aff39b1ac9bd1054720937a8dcef790e2403e06a4050c3b6b010a4ab248c0977c2166d664730a2906ef9ad054e67eeb3a7ee0ded0525cfda1b7fa32eabf8db6a3568614cb284df43b6c7425274600640a2906f04ad4b869feb0d38769cfd13e19951af0dd578347668bc1e3bc9ae30aa8a40dbb30e91fd945d2c9c3610a2906f137e1f37adf73406effb48db6cba3121d53069cba66a60c5520670f45e695aa00b2bed4baa9da78605d0a2906f1f77e263ab7336fd9d93c66acf735a8f3eb14003f8ff31a64ba88229472d8f92698edf5cb1a259df6c504bf6cbf91ca21b673aa3dcd315ad5acc87e8c8075a85f415df446a1e81604024e4f81041004386a0e92ff3a8727be9a486a763e6d4f8106007390ca97b17700e39cb2bdba4c640a2906f8f9a16f483f59c679bcf6e6992be5a708699e4958b33c9d499e91d75c920f30a5bba0623457da71600a2906f96968469e1be592db3f08bd5bda89f8306eba231d431cb8bc9a59fcc0269f8b0068c694d0250a5f0a2906fad5ed4cff2addf80b561960e9c5afd0f0012df76b5f659037b03f1729670e1027b532be030033021b299f295e7c0a2906fb886bfade9763bd86876367cff28817b4533e6bbff9748ab0a887f2af76f7cb840a45833e3959d13fe8fcba99b6f6625abac8d98f76c68c534cef92f43f56c7dee2de469257df1e142b4ef906d65118cd22b67c5edef47e54112fc1012f0e007989237c91709550ba8d2ac26a272e29dfd435e13e7c78677674f2debaa3b25a7174036be8dce32376256480e8fb58674ba75f012994f39c6f023fb77a367b03bf0ab6618704ae5cf1489a0540ee6154b3a60651d19504b2b3b50723703e33b2620a2906ff8cdc7c09ad57d6f3306e1f4feeeb6974b87796bb41e4bfcae79ee75e0dd81f00122acd00001eb0040033021ba04f0657973865ef69f3060a210812c206b304851900b2aa964ad0f03800105fc8025031400d8a3e3521ef00c2459705ebd094388d9dfe8715361d6b9dfc1fc97fda15d9e2225d6340a922a5890721c99186b4c5208bb65f598c097310880a88be504c02a59b8f560646bb2e27401702e44ca82194c2e6049080a2104437921b19168c608468100a6a118494bb7120068a0d0f663e111d17320c483f2f110068984892c0a15b6184e39bc183bec44c103e8eded377ec029c941da75fedcc848df59eb8ead2c951fa1961815361e8a8c335446463a6c011819a0e2f804614a92091456e86cc918ec1c8d0c47b2678a11dd865b5d4c645eba98ec36ce9e6a395c1394d39fb80739ab3e68b5eeca2f964ec3c26bee5a72d5a97f30c84ae0aeb6a45a2ac54a2ab563730d6e5bc1330057a2ccfd3b3ee78259ec7f21cc5bdd7638a51c86fbd96ce70cb2cbd59336b728d29bb9c09cb9822fd654dfb66bea8c39c5a723da279ca52f43bbdcf52dddda8fba7f759e37e3f3b9bfc53d16f9e43d952262c9d224fcfb68ac7f24d9cfebe0ee50dc826ecd676fbafedf14caf02ef249ab6a2ede2f2f1f1d1a13e9f557c3f7fcdaa5d4c9e5a48586c17e32b4b2168d25fefe79f69dde9d1f1fddcd974bf5db48e9d74dcb5df63dea657145be5a7ad6661b733339f1c57d4319a7a69d65bcffd7e8e8eb1933db3a6cbda71cdd669f7dbcbef3c9eb7ecafe5b78b38bdd971ff99d69bf612427f5f7777d7f5342a67c7e3d86ad6fd2e49096f268308cd7a9bde5a3dd96bbe58b3adf8bb6e15ae4071a5aa5240720443575191adc03094a1ac8c61a802d6951bd8b58bc9eec717ad991d8847cea3261661a4c78887101007253234c08eab2e1f0a5220200220404c9956440911b12a5802422bae30125acf8334e0bcacf2498a6fe1005e1930550a7e280d67bc290450a6443273016aac5448500062041c66b0368cedb157cf5e2618691b2aad85f01c84d2836b0098bb89480011e4b1e28d9c0ea20490a1cdf7e537a790df33816d007456a34a502dc98050015e0860df6700f1206e8c3f36f8955099b1b832bc5c28758f792ea822f11a0ba97a300f3c48a261b4800c8cd1254c7ee5c6090b815b5db350bcf6415e6f5ef0f6acf425bcbaf1f8b8e3565c0b6cd3c358c4022001a862881e810c922377bc1a010020917dfe6df212c17d1900b26a964ae0d2280020d01fe29b8ad622909cdb061026c84210bbad6c23f8b306651a8c205792e4ff63facf727e22c927fd1a4ad8c6e960cc3ba805f80a214364612e26aa9ec886cd40b5e72c52282237172dcd6bb6a2650059e00a22642988388aa9c1a276229460064ec91021267cf800f568f122e6e0800ea31f120324502f9d12f047c6284650d18d1d2298210fe58e1c3f6c7a60c4c02a108f9bb7f49a8d808fb2e3f3a995d597486f89a3ae9cd1c58688c2254591098b408121387c76be47018d2171408354070d2641440042344c866616def2c18d29007b8c76da7868fdccf19715b9b96869705e43ca4ee0bca6a4f99a17bb663c193b878a2ff9676bd6e53c15fd1c54ad8094aa842b297075fb605dce53f94e9fc7f2fc2c69cd43f13c96e726ee3d1d12ec437eebb453865b55e42bad56c87486ec72292de269f41b5bedd77c4d9729bde474c25ac64e13557a979dbabb4df74befb2c6fd5e7626f99fa1df3c67b29d525afa34969e6d148be4d70c8d4275260f412661b7b6db7f6d8be5792178a5b05a6a360cccc8c8486742b98ce2fb19d52cdac563e9e5a4c57631beb1934b8f7ef35e7e79d25a8b8eefe7cea4fbeda175eca3e3a6fd3ef3f6bc61d822ff6c350bbb95d55c724c4dc7e629a6596f3df77b293ac63eb6cc568fb5639aadcfeeb793df592c2fd94fc96f0ff179b3e3fef2a418da490b8d427577779d4e9b72762c8aad66dd6f1416f056255c68d6dbf3d268c9defa21cd96e2ef3a15a8fac2155d61f946f1033d958aa93e108c3158892098fae8eaf635ede2b1fbf13569b5230980f366c5e16b7471da5082c70810e304ec38ea7215015220200220404c9956440911b12a5802422bae30125acf8334e0bcacf2498a6fe1005e1930550a7e280d67bc290450a6443273016aac5448500062041c66b0368cedb157cf5e2618691b2aad85f01c84d2836b0098bb89480011e4b1e28d9c0ea20490a1cdf7e537a790df33816d007456a34a502dc98050015e0860df6700f1206e8c3f36f8955099b1b832bc5c28758f792ea822f11a0ba97a300f3c48a261b4800c8cd1254c7ee5c6090b815b5db350bcf6415e6f5ef0f6acf425bcbaf1f8b8e3565c0b6cd3c358c4022001a862881e810c23643aa41a010020fe7feb70ef12be651900b2aa9549d0d2380000851b3d48b6d25048068aca13d28e14bab0512a1dd04f89a8bcb68b323af97f94ffb4c18b1417e937b5c175bc658065048746b1e5da6848e22516d8d4312caec80a1b2e3ba29a87f6059bd9c81133c34144879f1e0f4708038f07152a2a9a142c02cc122e43413f431c95413362c10e89180e0b22131924440f0b8b17b5931a242806f81cd800f4d21014e38649ac82d071f3966e6353308fecf87c6ac70a0be92d6df4a5a607cf0903746505c48036340305f4013b0229302adc74c85cc8b0a245ebe736ea18ca6b06de12028c1900bb8c76ba3868fdac719815d53cb42f9bb790e3079bb794345ff3e297994ec68e53f125ff74cdb85b179ee848ea1486232b31755a89abda17e36e5d83cffb3a56e767496d3a90ae63756edabecb19c576f25ba79d237655d1575a9590a90ac9dd425aa1273a8cadf66bbea6a390486e39602d65a76922bd8f5e7777e93ee97d6cdcef65e791ff097acd6fa51b2fa4d59e587abea13824bfcab4c9c4953a047984dddedcfe6b3b2ccf7bd291c06aa9d925251f1f1f5cc9e423c5f7b3a959b48b45128982d6cdc5f8cabc072d3acc7bf9e5496d3a747c3f771edd6f07ad63171d37ed7795b7e7054117f9a7ab59d8ed5833c931351d9b9e49b3de7aeef732748c5decd15697b5639aadcfeeb793df392c2fd94fc96f07f179b3e3fef2a431ede4843699babb392ea74b393b0e4357b3ee378808aa56235068d6dbf3d228c9defa20cd96e2e738154af5812b7a12f9c4f043792a25537d2894ccc12a44a1521f5dd5bea65d2c763fbe26ad720500ccbc59310813692488895cec103257b0dbe88bfb043a624f9ae47fefd170e912b8351900b22a944800cf28006965319ecea38f84186d5182a1f403f51f28def90df0ca40450552caefff63facf527d22c927ed94513ec106c72d288a2108b92451411a962e4cf769714ab157b9f3672d1a350f2b4b2b76c20343e41062272431c23b8470de45091c141778742a70887a7630340d470b940b8291089d1b161b5258f83031e185c04d8b7f406161933247849367d4f494b868ce8c9796f45965707fb0dffcb3c2682b9c96b44d568cc88acc531123402da05a21a3f301716204549341e421c6e687de724a075216b40b8525065ad204a60470fd25cb2f0e56cf31deaa1a350f2b8bcd5d46188ecd5dc8896d692f2b3683af53a0d78e9f5731eed68922783ab99b4a278f52ea7442d53e1777eb3cbed1d7a93acfe49c75a2ae53755edabe9301b968f0579765d85d518da732da004f9591bb75848423d1612aad4fb12d1dc51bb9c980947ce592e4bdc346dddd21fbde3bac6fbf938d41fe19f49adf42b45147488f44d2316dde707c1aa149242ed425c0a0eb9566fb6ded90ccf6a4f3405a67d924241b1b1b5c88e4b079ed6352ab64f68ade8882109abdd7be46222d3acc3bf9649eb30efddac78d41f6dbc1ead745bf3dfb5de56bb620b81a3faf5a75bdc228f6fcced26f392a69d557c7fd4e867eaf8b0dabd45fed77e2ea31fbede3370e493bf6cff1dbc19b2df6db4fe609a17d74d024527737c7c9740863bf61b86a95fd1681986a34e541abbe662b9b5eb5f4c189ebbccf712813ea0353f303e413c3cfe4a7d30bf5994caf522a34994edf4cd5be96d92b76ffb5a58c72030448f8925eb025ae66067c6c5eb4e495ebb6c9720d01031a41bbcb7be9b86cf412c38d1900b2ea964ae0b23800905c3286b09cdba1c081200be0da27e0d6b70a210a0010388c03a784d8dee9ffb5fca7d62f525ca4855a831d45185aab801f8c08e5a619301105d7fe9df0b20867087ecceb16a2da8f37c615ebf0336375426573232208c8500b1ed2904e3762072806110c0274c21d1d3b420d8270f8b183a7008016090990494f50891445507e688c846a7c7c7cf0e1215280a086cbcbaec879149f3fbb11f210fd39ddf596d635567cda6aebc64550592223040e234760e4b0a1791d41d96041e22b813960a28481e88667c1d6c35f2f3131119f0f66d001f961b2f4c28bf7d39b775d51edc71b637b19b354617b99b36a8ce2dc8ae5e8fc1e6cc6e64e5931eed691a4783afd872aa71495d4e984aa812feed6a58026b053759f9e75d69d749daa7bd4b63b1cd20b84def76569995956b4adcc56d015c6e46e28abd144feccb5775b8dd17ff1586e38a23d6129fa79fb4b93bb7b096f6f7fe9d9f79fae49bf53f16b7f2bd14c282b3791e76b5acda3b96d8d7f1f57ea4c3429ffa561dff8fa78a61842e789f656d45b5a46a31157fabeac196ffdb94a8633e9b12458d170ce186672e0e4cfec3ff799d6998f9ff1d6ae4978fb8bf7f3939fbdfa7e987d532c8a2c73a72c57e5bfa5d5dee78a7e464d2eaedaf7b5ef3fc7cff9492faf7d987faefa7e0c6f7f73bb1e4f6cfaaee6f617738af567df675a35f89b34fe7deeee1c87e325ad3f8f23cb5578fbc94a58b31232b86adf14cbaa77b1dd62d5b7e6cd71a810058aa9ba6005244730fcd3098602c310a6921ac3f004d6540d7419cea4fbce1895598e40800d1bb5597491ac99811e144228a413f2db6aeb5faf6d55e5bdf48661a823d83c831280000024028000026c401100c3189ce13010a34822c9b20113c087e2c0e03910048402903088422004830002002000082804c0080092800a11a1c8eb20002bf4a2b1d8d558c044893bebe66d7d9dd41a07e0596179be7dc0b722c3182c3260cfad4067895bcce7fae88f62e81d9e0a2bb62e2478b3df9e738156b75dcced9e3acde70dc0b3c25678c2c5fbf0f3bb9bce926c42e0c9b06bf3160ef2939fe39e3f272834d091e05774101ef20c6cd1f34786f4665d8a9b875e4f21ee76468590b694c39d598288c3800a68c3019681c4b6a51c710635420d0354f86ccac74829a25511764806d2a0434ff9a5ccaa741805c908a8c0034e0259156f0c2419111d2cc46832abb2d3254846400796a21b6455f256906c4474683de213c99670ef5170026910ba5792bd1e070c13fe8dbd6dc18992e9fc876d0666747bb7aaa8cae55419cad2de55a4089142212df75d68ae28a954791e23436ecc324868a10113a87a4af7b294f8fa899c728d815de74df60fe2b7d88f92da4db5627c80fea5d0af40242758ba76832631d4004aee87b4652743106b3865f9bb02bb0255ee879c9eaf44e981f8c49da609871e6d0f9446c32a50c059729e938bd22d5020a3f739944dc5569c0f5d17447aa00fe3c8f9e76cd0657db87f838428effd54c3cd6cb52000de5c6ca64a3724568704f58a110bd7fca6d7be10a9b72009dfc17fd3d5ca45c7780f68e2067f061854075efd011d264af2c7d3991a1efc4266141a25f43ba91af0c18b0826df50c24190cbf643faca0ff28b2480b22b150881f512f98d0bbd21f383bf79c5adce4e9a2406d027137b2783f40c320aac9f52e35be7ad9ed02655ab1868df131f07eada604918612aef549474fdd342c38552429ff34cfa89a300e406aabde550a83d6c1976bfdcd87beeb5df8c9beedfbcd0d879ca6c0da209f7e0e93eb8b4064d4e9d4485f36e1b296ec7560ca7ff059da35323da69609d1bca3adc9cda083926fad590e7e1ea57237636fc749a54d9f71708b0b089b01613b097e2ddc571458d013aa1b57290ec46455a13915239aacd690ce21b42646e1f39a3942935316f4f4a216ecaa22156c68d13235666363f5a34d6383e1af468b37c94b3c23b26bf0f8ecbb88ddc1c9760e78e6475d660fbcaaa6b236a5c126c9c532ecd1a2cf771d1b551f5c735e4c7774c3ee1a253c3e00295afca306be88270834d3dd517d750826454ef8a38258370e78223c38a151ab2fb4a51c43a62df443783459cb3de53993b6d11e81488e83aab6218748e1831a0bcd008b31fe7565ba0a65a5b8ad67c55e60e08256a84a59fc0ada08a78aea4ce91fd62a4bad51a2130174c0b6913bea799f083cc51bcb4f8a0f292c092caec668cf08fcb0af5b1ed20e83f7dcc221f4a8be8c0e62235455c2ac0a177280a147c8c87847ede04b2df40cf95974c69017a61cf5fe74a295ace577ba810f7d39f6129e73a7fd207bd0b11daa2782ce1a6db614a8497857ca2ac0564c660f886d8ac3e057a06a138bb865660a0781cf99b6696fc1af87e18d7d4fb2d698f1e67a538d79219d0f6478211c4775f3d88c5321e72e941dcd01bae61ccd01c1ba0757a92c5475a78b00aefb9494b9283c8fd974b74be466d33aba8b4e7d5faa24a1e4ae4f9e9c0bcf28877e19a07a69de139a0a9146fe20d6813e22c45cabac16c0d5fb4b91975170939ca89879775d87e0f4f7333a92c127254b115c5ac25b72968125d73e17fa4f1bcfd2f5b8c1d67204d790f9b85264637830a2856948d7c625c4bd13efd1c55f257bad2069d4c9ab0f9a8370f7eefd2298185fdd680c72d5176d631d15bd9781eaee1097a139e6f4a2a48e02d62dc644cdc611303a5a4b8aa559a6c4ae8f32dfa5716091c2a1b1e326c864840ef66a027892f1b9d478b8b30126587710e5028d40fe923e29622dae6406318fdec56713f93a54f882cfba9e4495c4ca0bec156811ba5eadf220732e7b4495c37bd84212b54c07b165001468e09ebad5be89e4000f71a0ce7063227d3130d970e895e25df46c635dd9a6db7d425160d79ea24975a760399c709f1b1d9123cb4b721129bddb63104f79a827d303d29a6a06313d8e999a2c4265c1a30edab3dbb141e8413de06d7fb68213ce8477db8e9e99773df06d0ed0f6a06b945a2eac1265a87a515f0d82ae56329119a4ac3c1e75ec242002f7134ad22cc027d74cb2d304936dca4db1c86b6fa6f29a5eae9b8f30321c141b44ee1de46e14705a5307072f5abd21b37d77310e06221ae6455ade08a0c5e82d83d9cd557cb0b569187139052c7ad705505db12377602d2355217aeba617aa246272475d1b7c295babb45d4d80949afbaf0a129a70f3a840d312da897afa909d08471b1c83317306f69ca776ca379401c5165c5ca80130d5137235da7204403b6c1caf4c0a26b83c0ec81a5d20a4285621786916236d2d00233cf426f9ce5428d48492fe825a37063f924052329e805598129f80dcba20784be53b0e2352a0dc9685be30aad568e58171ef70cbb888414110446b20ad24f0e687bf6660e8616a59c9c6415448e39d1f4a4da6e429b1a821c263664102a0386b8b8a78e6c4ba0473c2ebd0fc513032cc53484571f1874c50bf1b4b8350c594291dd24344d14217857876930bdaa9785344a180c7109a1b96a5b021da7eb2afd5154659416d3009830152043d3f8aa1a6697d24264a27f61107bff8a344b30dda83c7e3cbda24342e327eb379c6145a48eea5c270e2cc645547972a800b7d5c550af54a15fcec5bcaecd2ae7cba322dd94a72fd324e16241a519accf52df8e871ef3ca3762a9bbbc90c62faec75352fa8218dcd2ac9ba17b7c55bb03f417dada6e6c49595077a99c65041e02d4187fc48040c790cfaa1a3e9d3a525679f9a3cbab56d4705f3314ac938bd7495f91805f2f588e321fc42ac94cbc0cdc41cb3dc66a81c40d69a619b10237103ce708c7db1cc242c3850f8bde265df1ea5b21aeeb859fd4aa7eb7bbf22b7b7151e0e587b42ecd56b85231b6480d7a21ed05550f5717423552033f207978eac2950a52476ad00f494fa469b8ba9299901af801c973f1842ba8b94b6ed00f4919b602e18a17aa00d9801f92402c8943534e9b561212935c864122808582a995850fb65c90977136cc2be38e6c72500da537f9ca8cd89787e12de29ab26d4bb944193e591d4c2db6cdb55bd40177a5ad55325586d14016530f3b6457d9fb4f318f2c64ce8025d3ab01c10e6591c29068bb8a7f9c8b79ca424a236ceeaa7f9221f294c554c16ab1abe8294b314f59a4300cdcae2adfea629eb2907a74617695ee8411f1908514e007bbabee305891872ca40c21985d4597c08a79ca228561c976155fc228e2210b290d81b6abd28b99c85316530540018f5b2d8b396121b6126c9b36e028e15ccf4410318b301a378c7adbf1eac5bc0db0061e1c37c110861c3f58f7476ae93a14f1e3c8ffccb08413096ee3e95756b30de20a13708cf9b87ee05fa00c52e85c18d237ede07250c44cc79ec54ba0e1f6790357a6aec7684d06af16366e880fa916358d6eca3d99bc9fc75904262241a40a0b668d1314b584a142c56daf298a83fa97c6d900485a0307318c4f078b1995adcc69bcb8a7bd43109e3632a3e25837ed22c8a7045c99aaa93c5b02a283cdd36f12cfdf85516a15f3efe33084980cbd6af024cc88424c61ab4045333166b1929c63b499e1618423404a5480a04e98a3215059ee3c29b38ee298e0a776f98047545adc0dc7dbf0a780277af5330d9d05f0cad5fa532a02d8b83b0c98f106fd72c04a4595c4883deafcd0828a38518eb6215882777e940005858e167d64c7f0f29f8ad068437f159f468b54925037ce272764e4edf4abd262e26b50ff0a9f6c4b1ac3adc72d6927cf6b13a8c327bfa244df9cfc387ec9c89694950ca40033baf56670715c081c95e9a0bf3adaeb3bc38f128d97a21a6613a467462d89ab31119b9e5284d030ea2df5f0350ccb06bedf04ab11f908bd7311ddd43394af6c20111b099a745118b9bc29782f25a35ac06080dd9576a5f7ba1f3583b681dc6f3b890866bc6505f50899155fb93405cd3f354ba1fec5fb3c77a93ac27c039a98a9ad399b2963c4a822b24c5b8416ab00ae5246b4547e6478cc65746c58f21bcad49d9162190d7acfd93b0a6de215fc93f14991ba7e98e9f895d8fb51d43119742a209d06ac378c37dce418c9d0132a7797926f324a2bdfc1bf15eba2590fc3fd8baa6658d17d6e4418a089b7a04d03af20657d705b5c39ca6872cc7a505c3935306e50dd2f755a698d3bfb69d96bde3df5ee29c4adb115f69a754b816dc371df70c3fb66dd52e6d1d68413bd9590e7e13a6e5b0181437121f6c0b0fe5ba1400161d32606c712d2db3c04c585b1714c0e65b322ac18588df6548f7f33af4657c761ad2eb50c316eecf80f9460dcec039b97305a249210127efb18b18e67bffced9f4c47a6732cf4cded722298ff10c3a10d884f49440633f0b757e7ce4f9164b5239472fff3a43ae61d232ae71affc309341fdce2231115b23901ecf7020cb305aaf0060a210812bf6d954ae0d0380090708c3bc353cb7152fb4033120110e4004ea202879a317f9a46b64249ebe4ff51fe0bea94242ed205510a0536008fce6981a6da06225c0bd360f21449311eb1b26d577e6f01a39a87d60536b3d119835a278c173224101c162d40804c84d84c76743e6c74209181c7c50711d00cb0484824478f50d10a119f9d0e0cb30b126aa613003a687a689448614086a7546265081c47afe935d601be911da75fed58c2c27a4d5b7dd158180a02aa31e42106916b509d5fec83081c1402665644724278401f1c1c7e08748cc565065eb3410c3ac22e3bd3080e5a4f691c6647350fad8bcd5fc81184cd5fcc9a2f7af16b8693b1db94f8929fba66dcad2b497daa55c9e7e12af49a3251d5bc1877eb4abc93d7b13a4fcd5ad38d741dab73d4f6dd0d299693dffa998ed8558ebe3c4b985c5b48ee46d20a4fa2c7d8d27e992fea2c2594db0d589ab2144595dec75377b7e97ee97d6cdcef6667927f0a7acd6f26da89a4d527d1f44cab3824bf9c69148a3375073209bba5ddfe6b3b34d3bbd295c0d256b453523c3c3c3813cac78aef6754b3ce8bc5124a8216ed627c6527062d7accbbf9665a6b2677edf796b7e9a9e692e38a3a464f2acd7aebb9dfcdd03176b1475bbaac1dd76c7d76bf9dfccea179c9fe4a7e3b88d39b1df79b69a5d04eca340ad5ddcd71376dcad97118ba9a75bf47525fad7c30a1596fd37bd692bde58335db8a9fe34ebe130f5c5505294f0cbdcf552ad989f77db205abf0fb545e5dd5bc3e2f16bb1f5ff42cb901012a3c5a621026c2bc10b08c28a0620304ecb6faf29f332db7f7ae7ef09b6fed12bb4d9448f0923800928b43acf9fe2f43f369d6039537cf2e7d1f1c8c6a7bffb203dc26c9ff23f94f1bbc107191d678026c529f64a651f037c465b26716689105761630091940975b3660ee249e9a8b56d58aa964347e902e0ccc985406094cc4c069a16054130088f55804f970c0060ed6d8c1e46233c3c14ac40e0d19421498cd4a0ce00133de348050214545400f1831354476545c15058e9bb7f49965c13eb2e3f3a985d596486f69a32b1a548f11cb0d021b276858e3064a86cb0102a593b32185880e95cb8869edcc9068d78a2a16de5202cbf4607fc94e0b1e5a3f69bc653d3517adcae62f242cc7e62f25cdd7bc7815c3c9d86b2abee49fa91877ebc6312491dcc11352c98989442ad53e1777eb4a3ed1d79d3a3f4b3aeb46baeed4b969fbcee6e3e290dfbaec84e154f57c9555984c5b48ee46821245a3b36cb55ff3357d058472b3096bf93a4d0ff40e13757787ee83de618dfbbdecfcf13f43aff92d441391a05a34969e69148be45799f63c2ed42dc81fec9676fbaf6db13cef840e14564bcd4e49090909e1429ec328be9fbd3ec92e1e41280f50b48bf17d8922f4e8acf7f2cb93ce5a747c3f77fedc6f0fad631f1d37edf796b7e70dc314f967aa4fd82dac6690636a3a36452a7d7aebb9df4bd131f6b161b6faab1dd36c3d6279c97e4a7e7b88cf9b1df79727c5d04e5e68cfebeee6389b0ee5ec5814537dbadf2323b05629c4f4e9ed796514646ffd90664bf1735c092c7da1899a8cbe51fc4027915ea50f045f27261104491f35d5be965d3c763fbea6ac720400ccbc5971d81a55e0f0f05af1028f70c16ea32bd709cdfbb6d09eec8b71f249d05471022476e30f458207885747e106041e8312a2ef2a2eeb5801bb085b95b6a5944432691288d8fdddfe1c5b5100e2061138b1108066448d4377532a1baeb2695b408af25375190525b51f6d4d2b96c4872bc14881972080a3680fb1b040a26845d08c9ff104a5018c594a87203740437a20d091a1060d032cab1e0f45021cd0097550ece00122c736a2c22603c9ab0322e3aa2e80febce6cfec048c243fde9a5a586d8df49a368ae37364c08d93d1b7404888080d83130f784610edfc5895424e01a9a11033007a5edcb86ba626265e3342ce39c2ff926d0c2fdadf3edfb225b51f6d8ded6d4a580bdbdb9834dff3629c1850c6ffa1e25beacd8a71b7ae85a42fe3288a2897507c215fa71ae8e26e1d0be8819daafb6dd259a7d275aaee4f9bee7e482e1159db976d1866d5125d65f542a63425775b81193df2736cb5bae67bfe4c29e6f62356f3b5cfafa41fe6b9bb9bae2ee9873976fda667d2eb2d7eed6f269ab702e31e697ea6513c96ba6ef0efe34c1d4a26e1b7b4ebfa5a1fcd7d257425b15a7afa6a656262c299be87517c75fe5c25bb982cc5a4c0d02ec6f7e54170f273f49bdadc74e6e3e3abb367d2d5fea27dfce463a7ae9f46db7d459155eacd72157e0babb9f4989e8f4f4fc655da7e76fde6f8183fe9305bffe58f69b61fbbdabfd49e47f396ae69a9fd45bc6f7eecdadc74c7bfd4f1ef7377e7b81f37e5fc781c59aebada555cc25aa5a0c155daee2ba3257bab1669b6146b8e438528504cd11617901cc1f04fa7170a0cc39783d418862790a66aa0cb2e26dd35bea7ac7205463cfaac586c91363c0e4144294c9015f0db28cec7085ef0233daf9c70ed49f0d02800d8674ceb7e223c139463e026873ec4149fc5e27862105549b5841291f6fd7f4cff59ba4f24f9a4b73b0531137f0700001c821648294020467b8e9843d2740e9322058195511d340f2bcc4c8cc7c74781880921400ac86c0899d852046861e2a347c6c98b0d0e25482c033770845e1c18d18548b080e1c212e347b5c2d521a8478517542301c4d30032cda0016021c5867ad0f1d2928e53596e0dec37ffac303a134e4bd6a6ebe67342540488044e60d030fbf063cb74a7e4cbc941418207417600f492a1e5d5362b3031d0920b64ca01d75fb2dcc0c1eaf9c6676a07cdc30a53f39811f653f31872625bdaeb8ae9e0eb14e8b5e3e7568cbb75de08880a4f22ca672ae527928fe6d970b7eee495bc6ed57926274e17d275abcecbda773691cd0efeeab20cbb2ddaf1544619e0e9327237115658125da6d2fa14dbd2574627371b48c9572e4da37758a9bb9b647ff40eebdbef6463917f069de637d2ac24c2ea92483a9ecd1b8e4f6bda64e2481d08165daf33db6f6b8764b62add08a475968d82e2e1e1c1914c0e9bd73e36f54a66af383a49c09ad97bedab64d2a2cbbc934fe689d3a15ffbb8b1c87e3b58fdbae8b767bfbb7ccd16045be3e756afae5718c523bfb3f45b96527af5d571bf93a1dfeb62c32af557fb9db87acc7efbf88d43d28efd73fc76f0668bfdf6937942681f1fb4c9d4ddcd71b62661ec370c5bbdb2df21251f8db6d0f4ea6bb6b239aa963e38719df7392ef5a53c5035474a3c31f43e47a15e29effb5e24aaf0fb50de54d1bc96d92b76ffb5a58c721fa1085fd20bce88392ab8a8f854bbf072bd365d0e14796c159aa59212c0751900b22a0000a445f0c903d663e6ad87a8289759a23016ef213b240784950cc5b714d3ff6bf92f12f894e9297f6f7ed85a6787d0948e8352b09d386a08c91ded732a7081608042036ceea2d98bd7e615fb31651a0869b1422bc8d941d33232e3c481878a041d500c0b34804cf4e0d4303983e840428484d8a300051c204e0818e418b180223c0029321a65ac4e442306c8d05630fbc2e7516cfecea5c943fa73baeb25ed4bacd8a4d51b1d024136787cac9298e4d0490a2167e1ca39f2329e413f421810101e689db831c4613336b1109b105c2880fce09956f8f07eaaf3af2b9abd786d68df2a4917b46f99556314e79b988fcedfc1665cee9415db6a5c057f256449a5565e2505a2be6fe6c1b61aa7e2953c4ec57d6ad61d0e85e354dca3b4cdf18c601ff4be7fa6646659d1b6a79da06b4db9d5525cc4d2f8ae6befb61aa33fd3c1d478426b82296aeaf69325777712dedd7ed2b3ef375d8f7ea7e1cfbe469295525cbc349aaf65358be5b6386e326d246e811ee5bf32ec1b5f17cd14cb705d686f453d26a6a4a46423999eac196f6d72d589f3d8c1387091e19c31587af1f15dfbcd6da675c7c5cf786bd723bcfdc3fbf9c7cf5e7d7fcdbe290e4356b95396abf25fd2eaee73453fa3a595abf67dedfb4df173fed1c96b1ff4cf55df8fe1ed5f6ed7a2894bdfb5dcfe614eb1feecdb4c6b06fff2829b4ceeeedbc6e324ad3f8b22cb55783bcae99bd908185cb56f8acfda5d6c7758f5ad796f1bea437961aaae9cbc51f4be6f690151def7810f52e2f7b5783535f3fcc47974df19a3a7dd4c0268d8a8cde16b4cd9301242d153e247427e5abd791801343f37f5d8f1c5ec3600cc16209f47a76d1bd9168ac96263232db57e96e819fb76dcc908db224aff8ff27f6d7c91e222dfe90184e2e5410813830aa5214819aa28ac03503848ec43d8ccdd1526580b1d330fed8b2bf6a1eb508b120405b10cc110ba9023850c1648d380c0f1830115d300123e8d132844e5c080160b21422f6cd82c186bc2122e880a146d78d51d014421554408142aaf4a82c7cd5b3a8e65c137b2e3f3a91dab2ba4b7a4d1981b13030858d45283132a0f14328c1e332b3e25a0152a867402b440cd70851c5662f46be52506def2818c09003b4c764e70d0fa79e32edb31f3d0bed01c861c47d01ca6a4f99a17c7c47832769d8a2ff9672bb6d53810e5999490a0501fa5231fa84acdbcd756e3463c93c7ad383f4b8ac395386ec5b9497b6e67f4eac96f5d768eb8553bbecaaa844c5dc8ad26c20a4da2cbd86abfe66bfa4a8752db016b093bcd53f73e9ababb49f7bbf7b171bf979d47fe27e833af91724c22ac3689a5e71c8a43f26b4d9f4e1b89539047d86dceedbfb6c3f2bc10b80eac969a9d92f2f1f1b1914e3e527c3f9f7a25bb58ec5018b0722ec6176672d0a2cbbc975f9e14a743c7f773e7d1fd76d03a76d171d37e77797b5e106c917fb67a85dd8e35778ea9e9d834a9f4eaade77e2f43c7d8c51e6d75583ba6d97aec7e3bf99dc3f292fd94fc76109f373bee2f4f4ad34e56e8d3a9bb7bdb769a94b3e3306cf5ea7e9750dfacb6ccf4eaed7965b4b3b73e48b3a5f8b72d4549509e187a9f9f9cc052def7c14a54e1f79d785435f35a76b1d8fdf89ab2ba11f0b079b362d025bae9c727a5abd113013b8dc6b8894aec8483a37eec906fc912989103351800e2eda046f0769b0094b1594d1db81d1e5ed0bbcda17636393c27238e34cede51a50d6d3aa1e4d2049288fc992ceb4253b1123682191485b5945cda923625c63b8bbcc311c57120ca0b052ba8cf21ccc8f442850d84152d341861d0e578620a3185c9021d16478e0b114b05419014556464847a08c93b921c6820a0418d04063c9c18b23992142c0a700800815249642852a0890d0a34ba06ce8b1bfc9af00297323e27fb0d497a94ef06cb3b53424402184e7cbc8ca091d362c0018f18415ce0282a40b4d8a42c51f242c30706999c1f93c6028c096fb0020771007fef44ca3c0e9f2cf1a37005f53984293fa6348ecf73169168941f03de718b9bcf987046fe2634dfa54e8a4c55caa21f4a310f4d95b2b8cf68e5325aa9b681ea66b39a3dcedc7b5b46358bd90baa5e76aff7b97e9addacdd388bed67d883322d7fb9ab5f6dbd5bc5b766ee7a1404eaa8c53a6cb3da719da5f4e957318ae1af6298067aab7d9f1f84ede6d59a318761a02d6fb4fba8c67956e33e2b643d09de1a8bc5b242d68b65b76e72d0a0187bf82792c4457aa5eb93de30de0ca52a55a53a95c773d95c9e4b5ddb7c2044e87adce2e7cf35217523b65cfd4935faf173ab83e4ad99a450f3fdcdc7e1f3ff9ecfd9773833486e6c5d5887579c2e2e9d4ea7f2b43ce9f2ddc796193a6dfe5d131e44b0d99c6fef2361f2cfe18f1ecf85fa97cf1b7ffe946fbaca8fc11af4c8999cefa7d3a93c33c4bfbce22561ded91fecf490dc188b4a9d14cd10ff90a4c7ebf9159f8b9dea667ab40da5317498a11e92fb74270b29d4bc8f66a87cd3156eddf11dc3cb7b55711b477177ab46bf4cb7b7d6e3e8b6795997b7cdd2dba1e83c6dfecdd9f9164fbad2003e7451733cfa8e00f06640093062f1c2bfbc337f232e00445a216269c11226b4e2268c2811cf0d4a337d4559b8acc004311940443345628418b51ab67c0952da813336f3c9885899848961e09431f34f3450de733dab08980bbd84801f0ba0a68dd7001ce23ff030511a46c81d5725c530c2ae0cc0b64b2172d40a392b95735703178a639a53d950d798120b181280aa18a2872203764e9f7e1a010020fd7bc9ea6ce947f0923800e88eb359ed64cf3c9d0194e1c79c58999516a949c7f9c9fb92dbd0f7ff48fe8b045ea4b84833e58021a62236c04e87420bb564bb451959dcfd0013dd51b637127736282330ff2acccab5bae910e423934b62e8f0f123c409d50e17d8060b0c2b5e1c00d12d504120ec61123d189071632311c262c6089a0f1c0a9208e8e00a68090d49e122824349f4035658d484062fede836b5e502c17ef3cf1ad2d537ed189ba99a97d64d0ed0468e1a1a2c9c0ca245810ba6500213454427ee8810384a1e30b81fcd628171797684a02a015c6f9199827bd5738dafea08ccbf0a13f318312c11f39871625bda9b7269c0d733d06bc5cf289736dbbed0149142df273281a4900934c13a9636db1874a06e3b6d9ec769b3956cdb69f332f69bacc3da80bf3a99c38ba2234f493a014f17519b99a07c20d05595d6a7d896cee24999c93c3ab672c979de435077f7c7bee73decdbef63e38e7ff61ce6b3cf0b6482d2207074fc9af7139f5a688ed33e1b02dcb95e5fb6dfd6fec66c236c1e8fd659b68a0a87c3d13e9c87f3dac75c9f487b414f8a029497bdd7b6400f1a74d5fbf8639e36fdf9b58f1b77ecb77bd5af837e7bf6bbcbd76c3d0f257e46f5e97a0d29f6f89da5df12b4d2a7af8efb7dfcfc5e073bacd45bed77e2ea2efbede237fe462bf64ff1dbbd9b2df6db3fe659a15d94698eebeed634597f30f6fb7da83ed9ef9293118c4a98e9d3d76cc9e9a996be37719df735ad342a759e69864e3af0eb464e22b54add68d46a60fa462352374db0ae497bc1eebfb624a9461280e64b7abd152822556346568204f9723d36537e1324d34aa5dd7fe9ca70ed12bcb304551900b22a687613f990409b1aee80160f503cc8291b9c058d0a1509b332fd5e0665b5f4ff28ff698317222ed217ceec9d08711364935868882d1ada10d0b6cdf354b60fa86cc2d0acee229a8b96d58a91090a2ca82f7ac25147d0303394fa1e3366662b422c110e2cfe0685f361c46d078d919f030862e0b87181048f8c92904347081f22401b0d002e85071f9c5a0b1757457173f396ae631de01ed9f1f9d4c26a4ba4b7acd19a9c1f2e1e103f065490882e4aac083a471b3702402132324cc0c8d0411404f2f884b4eb85150b6fc9c0c60c80fd253b693cb47ee678cb8a682e5a56cd6348d8899ac794345ff3e29ad82d63c750f125ff5cc5385be7a1c2946832a69caa546e72f2d13c1767eb4c3c93d7819d9f25d5e948ba0eecdcac7db733726dc86f5d76c2f0aa8abecaaa4ca63024672b51114da3dbd86abfe66bfa0b89c5b613d6f2759a27d23bccd4dd5dba4f7a8735eef7b2f3c8ff0c9de6b6d2cc54a2d2a6b1f43ca35824bf56e8d3892b75227984ddce6effb52d96e76dd091c26aa9d92d2d2020205ce9e4308aefe75383b28b47128b0295d9c5f8be4c2b3dbacd7bf9e549755a747c3f771edd6f0fad631f1d37ed7798b7e70dc315f9e7aa41ec165633c931351d9b269706df7aeef752748c7d6c98adfe6ac7345b8fdd6f27bfb3585eb29f92df1ee2f366c7fde54929b49313fa74eaeee6b89d2ee5ec5814570dde6f12948f561fdc34f8f6bc324ab2b77e48b3a5f8394ef5a9bc3085a250bc51f43e4fa55e2aeffb5e2729e2f7a53c9a42f35a76f1d8fdf89ab2ca0d1060e1cd8ac3d62834e212010dd5302160afd11aef09ebb597adfeeb6ef1f0d02800d40f71d17e4764bc8fdb33257737b3fcbc4e32f7c77ee7b166c428d992ff8fe93f4bf789249ff4554996947520b6820250ad28a84dd7886cdf114290ea457e86690d4da32d1f37172d2c4673c16328af902144e3d4402222091951c08f425e08bc6a305b8e1a1c8f0714008436d450c163e42a913a11a2d26306c7a14311490557061b26807666e04869b1317ac9a92280dcbca5fbd818bc233b3e9fda598d89f496387ad3438b91002648c780002d5ef4805182614724868204a20b2e0b222c8e52e80c991f6e740e0c8c16de52018e09825d563b1b7868fdecf198fdb8b9686138a7216721704e53d27ccd8b6f684019fb848a2ff9e78bd6e53c1415b862a1802eb244a72a04abdb97d3e53c940ff5792dcfcf92fa78269ed7f2dcc4bdf753ca09ca6fbd76cef0ab7e7cadd59a4c65c82e77c222a246c7b1d57ecdd77418932bf713d652769a29d3fb0cd5dd7dba6f7a9f35eef7b273c9ff0cfde6b9930d75c2d2a8b1f46ca35824bf46e854aa3b79067209bbb5ddfe6b5b2ccfbbc03385d552b35d5c482452774af98ce2fb39d5addac5a3c955c262bb185f19aaa547c7792fbf3ca94f8b8eefe7cea5fbeda175eca3e3a6fd2ef3f6bc61f822ff7c750bbb9dd56c724c4dc726eaa55b6f3df77b293ac63ef6ccd3590f10da49079d4a757777dd4f9f72762c8aaf6edd6f131578ab0c1e74ebed796bd477dd0ab8f2852c9aa2fa46f1037db592ad7c202853618920b8fa28ebf675ede2b1fbf1356bb51b01d179b3e23036821c002160c4a64868063b8ede388909328f45b5f1a26fc8129791032d1800e2ada045e0d6b601300f831ce10465900d088330409b303cc1e8d0b14984ca8ce0ede8cd9329b3ebc96ef929d8a4953f33091981121fd6a6246a429a753f56d84e0d267124245639062ba8bf3a26449a22e2735982e409051e3715930c3b86904e3f78e4d0008283900d2f5854f9700304410121286e7460301e8304a01a3030146859d24203101b4470745842505e941018a348a0c9344c3cb84b7fd216f894f0b9d8ab16e5d0adbb34d619120ba88c08d2c34912012914202922c5458c012e4629033e5866ac1c3072e465878a0e3a244d851892dda5091d4000fc3f93b8e1ad7e91c487f40aeaaf8e195f661486cfef0589668c2f53aae106379f219908f997c87c8fba082255296b76e66c80d154298bbe3633aacd6c6a1af761eecb3e0f87e9b76556abd77a75e33a8bffbbcee2cedb3cd9cda8563bedfb9e7a38af9b19cd6e876ff65dcfac5e9d36c39a9661cc7578ab14779656afbb5f038d7ad59b1af5a827b35ffddb6dcf7558fbecd2e7aa96cd8d7edd46b1f5b15e2cd593d562591febc1b15b363a684e845dbf491471505ee9d9943384f56554a5aa14a7c2b0aba6f23beab9a6c3c041cfe106ff762e08291bd0e5ea2f3ae5b7cfad5e12eb498a74e61bcbb7faf9e3e752f6ade52d89db5a97cdba823242040e8753612e2faa7cf7d0457e4c9be30b028b839acdf9fe3a0f24ffab3fc4b00bf5307fb7fddb199f748d2f6373e91367eead1d0ea7c2e487ff58c1eb81acb27f89d3b5b8ad058dba08921ffe5a94c3eb79059f839cca46621b87ca173cc84fd7e23655d922d299f790fc8c4fbaf4ceddd65057deab8a72745aaf66dbc47772df751f9d1cf769dee5b86e560f35a569732c65e71b34e52a0708d1c1cc6d081f09f0c9682220b40286ff5867fe462e00445a216269c11226b4e2268c2811cf0d4a337d4559b8acc004311940443345628418b51ab67c0952da813336f3c9885899848961e09431f34f3450de733dab08980bbd84801f0ba0a68dd7001ce23ff030511a46c81d5725c530c2ae0cc0b64b2172d40a392b95735703178a639a53d950d798120b181280aa18a28722033f3ea83a1a010020917a32010050020ac8060a2108fe6aca12993d1800e22da145e0d85401e4a92cc4010db127c34025674c8bb5a4cd09c051faa120c06dd698ccd8c4a6f5b49d3ee4eb927379fd089683310cadb249ba4b2849213a27e7f044c099acb261032ca7c7a38c8884800442a440112649960c791043550962e0a30446410e802350243439e023440a181f321393001e2daeb8c890990262458e986ec044cec191e0c44b890493f263400306156a748d124f6ef14d23064ea57c6ef63b865a84ef16cf4b33620687252e62e84051c34207152a9e087161258a8398b0474b4924c2c6821e2f3e2c94d09a0b3224708b163c9004f87f2593c383e39b235e34b29c1e8f32e7cf8ccaf239c6a190c6f933e22d37b9390da944c99f84e67bd4cd10a9c2b1aaa756ac5353e158dd6b35ebb49ad5f72e683bac596cb74de39ec31ce779b6ab98973b6ebb9bd7dd8f86379c314eb3f582d96737effbb0edb4ac612f779f339c7d3e18c66ac63e58f6e9b6fc79cb72c62a08769d7decde2f7f3eeb8198bb9ac5b46abb2c7f19ceb056b7eee3b62e5b20eb4df19a2c089605b29e3cbb75b3a3c644d9c72f99210f6996ae4b7a43796346154e8543559dce6573391e756de389f0d275b9c9c79e0b06e78674b9fa8726fde073ab8be635d170d47c67fae0f8fcf3734afb1e2916cd0d5a17a8c74b520a14288aaa3a2e1f5ebe7be94241a5cdf305e3e0c56673be3fcf04ca1f8f5f763ad7e9353cdee063cff9a4ebfc19508b9f4983f1f55054d5a120fee725af09f4d2fe22d54773836068d4cd1005f11f435d5ecf2ff99ca4aa1bdad9ec4967f840417d3477e95208e1a8791751d0f9a46bdcba83b71c2fef55d5d9ae82decdb69a71b5cf715f57adfd340f5bcbd5eb9d2a2d6d9e29ed7c93255de996109dd41c1465232d5f8e9300a2151af89f97e66f0435e9d39cf67fcae370ef12bdb3045d1900b26a9547f09414a80a8795b16dfcc92a9aaad8f99fc0218696713a14e4a05fd02591c9ff47ffc95eeb225f1416566c745b2c7264c9fa5664b177498725ca42c52f289e7e329f9a537bd38a61709184820d1d48447400c20a12224329c4c4a21019cd38b930bbe1d08bcf06078a03199411882127403138d49420d51f2d5edca8f1e104cd8614121a4c38c8f0008a71d5143c4eded167760136921d973f2dacb6e8bca36de6e8f4d06889a1475400a990327102e363a16203c886cc0e02257a78580a8c1060f341a25d333731f08e0a586400ec2f59d9c041eba58eb7eca7e6d4ded81c4784a1b039ce38f3252fce89f164ec162abee297ab1877eba869b6a04c94b6a842944915aa6a9e8bbb75073c92d77d9d97e39c752a5df7754edabedb09b97af25b979530bcaa9fafb2ca20cf1a91bbadb85052e82c5bedd77c499f11bddc76c03abe4ab2247a8791babb47f745efb0c6fd3e760ef997a0d7fc36a291565c9a148e9e691353f1ab842e95b85107ca213a9657a61381d54eb261604a4a4ab851c96113dfcfa5fe641787a2971417dac5f8be48093a74d6fbf86339674d1ddfcf9d43f7db41ebd843c73dfbbde66d794170257eb9ea0fbb85d52c723c49c72429a6bfb79efb7da48eb1870db3d55fed7866ebb1fbede277a6e315fba7f8ed202e6f76dc3f963342bbf8a04ba5ee6e8edbe951ce8e295df577bf55584eb54a03a1bfb7e5954d91bdf5c199edc4cf71a953ca0355d3c4e285d43b390af54a79a7d30b818a9e4e286faa6a5ecb2e0ebb1f5f5256b90100263c5931d80a7918707d7214e941da60b7cd1c5f11dc3b19a1c9eeb6f0685ced880ed23f67136083730e8d7b874482694481549c3819a47406b98ec9ffa3fca7d62f445c241fe86b5e793011095b8bdfc66da273986cf122826c0e162d61fc95f02101a29a8b96c6257bc1b45656ae1f5809126fa5038c16201b246e6c411532b0180a12096162e483cd8e0e9212e762b810ad00a2a1e1058a11337c583c7076fc7c48f888e921e0c647c4cbabf6f0b8794bcfb109708eecf87c6a63d525d25bda280b2704c30bb6e1c200bf22167666089d0050a384432bc06da48501192953c8e37afa1543230b6f89a0658ab0c36627030fad9f38eeb2a29a8b96c6e63564cc83cd6b4a9aaf79314bc693b14fa8f8927fae64dcad1bc7f0747207534e2829a9d30955fb5edcad43f94c5fa7eafc2c694e67d275aace4ddb773aa31785fcd667670cafaae8ebac2ec85486e46e272ca269f496adf66bbea6c7905a6e3a612d61a7e991de63a6eeeed27dd27bac67e835bf9568a61396368da5671ac522f9d5a63d8f2b75257984ddd285e94861b5d46c1717101010aee4798ce2fbd96bd5ece291d44280857631be30d3811ebdf55e7e79d29c161ddfcf9d47f7db43ebd847c74dfb5de6ed79c37045feb96a15761bab99e4989a8e4dd34babdeec98ad0e6bc7345b971fb4930ddaf3babb394ea74b393b16c555abeeb7890a58ab0a1cb4eaed7967941c0a447d618a1650f946f103fd7482a13e1084a5a444103c7d3455fb7a76f1d8fdf89ab3ca857c4078b3e2d035de8830a46226050e9d19ec36ca72100103903365b6f0a370ede4c7f998d09cfe8d0de81eb6f7efec6ed99e63e3f79caa6824d931f97f24ff05c117292ed2e336b20ad098ed3b3b869a813ddba61668e176e6339292e89d21269ba809249f998376e58ad1a0d0e16b71592920684c5664b04664e0c4c4583d5cf0d894014e2127af121e1b252a48bce080bcb809b1bae120070f11156a2972010426305a54254434e0487d807955143d6e5ed2752c045c243b3e9f5ab1ba407a491aadc9195023c68e0e9823a4584441091f6da28533a4252c6becf86040c09780356246907ec9ac62df25635ae6073bac3c69fcb37ee6b8cb7e660eda15cd59a37882e62c92e66b5e5c13ebc9d837547cc73f55b1adc67da5aec18a4aa9e4519ae27d27d3ac7b6d352ea51b751c8af393a43adc09c7a1383769cfed805e1ef25b2f4f11abeae76b5927643a336e359414380abd65abfd9aafe9322197dace5749d8699242efe2a8bb5b743ff42e36ee77b233c8fffc7ce635916c8492ea51487a96510c8e5f313489b489b8051984ddca6effb50d92e77dc085be6aa9d92f2f2424249b88e422c5f733a951e5c561c885414a7631beb0514b87de7a279f3ca94e838eefe7cea0fbed9f75eca1e3a6fd3ef3f6bcdfa71aff54350abb156b0e39a6a6637304d3a8b79efb9d041d630f5bb4d561ed98e37706c93bf6d3f1db3f7cdeecb89f3ca94dfb68a149a4eeee6ddb6951ce8e4150d5a8fb7d52f26635c28546bd3d6f4943f6d6ff68b614ffb6993c53f79da84aa90bc1ce73161698a9f33cd8ca09f43c968e9e665d971787dd8faf59d68d00809b372bfe5ca1ec07872c9b1dc012b0d3688db308ac88185bd1ecbeef335d92ec2a7fd0d1881be0a3f00191518111af23c437a90a16f641199dfc3fca7fdae053a6477e8f14b7e95092a113682c1a8b5d063b190965c74b39e16e595f7a913b0a1f350fed0c6cb6e2459784708ce8b9711d051d9041a2d102833900c6a378688588b19182870b578ca300f4067591f1d1a1628606022f0326462758780a2c04060538b482a850914bac0ef1b8794bcfb10f308fecf87c6ac70a0be92d6df48503a2c608e8030a9fa197083f3f398a807aec18698d44203498851222441d5036d4e818cccc0cbce58b8d09825d463b6b1cb47ee238cc7ed43cb43336a721471136a72969bee6c5af194fc67ea1e24bfec99a71b70ee5f3d447552ad14495023da5aa7931eed62df04e5eb7eafc2c694e47d275abce4ddb773ba4584f7eebb473c4acfaf195565d99b648ee56b2129e44b7b1d57ecdd7749812cb6d07aca5ec3451a5f7f1d4dd6dba5f7a1f1bf77bd999e47f825ef39b49e754b2d227b1f4ac4371487ea5d0281467ea10641276ab73fbafedb03caf83ae04564bcd6e69090909e14c281f29be9f51bda25d2c96584656742ec657766ad0a2dbbc975f9e34a743c7f77367d2fd76d03a76d171d37e6fbd3d2f08b2c83f59bdc26ec79a4b8ea9e9d83cb9f4eaade77e2f43c7d8c51e6d75593ba6d9faec7e3bf99dc3f292fd94fc76109f373bee2f4f3aa19d94d028547737c7edb42967c761c8ead5fd2639f96a35e6a6576fcf4ba3257beb83345b8a9fe3549fca035568eac41343eff3941499cafb3e99894af87d291e55a9794dbb58ec7e7c4d5ae50400a8f066c5204c74bd12c0901029ca2083dd465f6e22c2539420f7e470f34af0d028009813b0aadaa43fd41943e5dc152c9e4659228d1d4e4cd5ed000a401d255bf2ff31fd67e97e4ac9276d9ba128f1e28a01084020dc2322d1cdc28c13089c78e93d588e96305f0c1f370fed4b8ca69342a445871e4543276804c95264666610d0e484d8697df0a0e227078922e34274c600c23960200846880919b901a3d00d1e17b917056c803079483d3c361401d1acc86a0f8e9bb7741bfb00ebc88ecfa776acb190de12475f35aa1242300108e4acc051c147c38e159f19433a6e7e628860af2016e1ce081d14355ac6f242036fd900660ac03eab9d121cb47ed678cc7edc3cb42f388721471138872969bee6c52f1a4ec66ea1e24bfee9a275390ff5a14c4c684909f88528d50cb5ba7db22ee7957ca6cf63797e96d4c61bf13c96e726eebd9b912c437eebb573c4aefaf1b5562364da427639925668121d66abfd9aafe92ca494dc0d58cbd9699e48efa3a9bbbb749ff43e36eef7b2f3c8ff04fde6b992cd44d26a93587ab6511c925f67fa74ea4ade823cc26e6db7ffda0ecbf33af04860b5d46c1515101090ae74f291e2fbf9d4acdac5222945a465bb18df9909a54587bd975f9ed4a643c7f773e7d1fd76d03a76d171d37e6f797b5e1074917fba9a85dd8e35931c53d3b1695a69d65bcffd5e868eb18b3ddaeab3764cb375dafd76f23b87e525fb29f9ed203e6f76dc5f9e54a69d8ce9d3a9bbbbeb6eba94b3e3307435eb7e8fa052b70a6142b3de9eb74649f6d60769b6147fd7a952aa0f5c5113d427865fca4f4e66aa2f959a315885a9d4c94757b7af6b178bdd8faf59ab5d08000a6f560cc6c4be9000c30f0705a00b761c7d3989d8138e19acf299d0904fc983a84a9e8614b18a48db192c0c00543b402aafb7d658a77646f24f921ef9bd44b815aa440430f61c980bb78818a63ba1fda4a45841cc15951a0e1612a39a8b96e635038143c78a043e5422000a921420580809139c175000a6c141039da2132f64ecd4081987448c0861a385818912d78d203c1408813166b878896961fb01ba4856681080c9a92a7edcbca5f35808b8283b3e9f5a597d89f496367ab303c300101b0423441e7c2ce090fa4042d463dc9083c501560bac83cc88cef94d08ea1c199a5978cb189739c21ea39d361e5a3f77fc654735172d8dcd6b48590a9bd794345ff3e29bd94fc68ea1e24bfed99a71b7ce4b0955626a54395dad3ca5f2d5bc1ceed6a5bce5e94eb47de743ca01ca6f9d76ca70ab8ebed22a854c6748ee86c2229e4677d9ba4cc9e5e613d632769aa8d2bbec9735eef7b233c9ff0cbde63753cf0985a54f63e9b9876291fc6aa15128ced495641276db7383ae7e79111111e14c2897517c3fa31aa45d3c965c4c587a2ec637766ae9d15defe59727e569d1f1fddc9974bf3db48e7d74dcb4df67de9e370c5be49fad06b15b59cd25c7d4746c9e601a7c65b67aac1dd385b593151a85eaeee6389f365b0ddeef9395af5623e034f8f6bc345a6ef5adbc9045535657a9622beffb622a2cf1fb541e65d5bca626ad7203005c78b3e2f03506c9b0c3e6942d4802761bbd71a138ff9b7f8df0fa6beb12ba451900b2aa9448f0b23600e4ff0c245e7bce885ea1e2c9899cc1f02a4dd19b5e8bd1c286a0908892ff5bf9bf36be6c71915f73e6451046590a04589c021b32395a90d5e14808962484378112444d91a1988b1666f5ead92eec206105ed9cf0b1c17dc8a14339033c7da0c5ea1bc072c3632103967a9ac40b13811531201e330a20a0718a81a3838c1e3a2534a810b1c170f9094ae9585835448e9bb7f41aeb00fbc88ecfa7d6555722bda58cced0c4b8c03159f063856707ac4152a52061119f1950880e18a26222800e141b5eb466b5c0bcc25b32509921ec2dd829e3a1f593c657361473d1c2c83c86748190794c49f3352f9e79e564ec182abee49fa99736db4c3e377d14044712d014ba098c792c6db629f03a6f436d7e96b4661bd936d4e6a6ecb71b112b437eebb0d3855335f415562164ea426a33128ad88daeb2d57ecdd7f4164e657613d6b2759a25eeddd575778fee73efaec6fd5e7616f99fa1c77c36b2e94828dd8da5671b8a45f22b852e95b4d1269245d8adcdedbfb6c5f2bc0d362eac969abdb2020202a28d4aeea2f87e2e350a76f1c8a92440b1b918df5697d2a3abdecb2f4f5ad3a2e3fbb9b3e87e7b681dfbe8b869bfbbbc3d6f18a6c83f538dc26e5d35738ea9e9d8ec581af5d673bf97a263ec63bb6cf5563ba6d9faeb7e3bf99dc5f292fd94fcf68cd0a55277b7a6ddf42867c7a2986ad4fd1e29f962f5818446bd3d2f8c72f6d60f69b614bfa6811fe88527d454e28da2f7f9e9d402bdef6b919c88df77f2e849cc6bd8c563f7e36bc2aa26004085372b0e5723c982e88703ce8e8717ec323ae32302e4cd9764f27eebdf6ff3e0b23800c0762848fb45f23fac03c014d0014000f4e0441109204c5a8158614928fd7f22ffa9f58b14176951d81863ce77900b2217590bdba7b39a48d680fd48e2c12b261dfe348b400124b21f2fceca7580070d52880bfd48998005fddca829c574332b36f4f884e46416283f295e0e193c27740c098a21aab1030708032134c054405214405141c208ed83ca0681113e3ad0b0ac099f47f1f93dd7412ea23fa7bbded2aec68acf594dc1380028013a22600650101739767212886f6146e446068c162a3d6c30a888f030bae0ac1a1c97884f062a3440fed62b95f0e2fd14f6ab4b22fbf1e2cc5ec72c4bcc5ee7ac1aa338a75c3e3a7f069bb1b953946bab71e2085cf0e2321a85b5b684a2a92403595b8d6b013d903b719f9eb5875be1b813f7e86c73b410eb47effbafb4cc284bb2edcb42d0f5c6dc6a2c30a347beeadabbadc6e8d77432359a68cf568a7eddfed2737717e1dded2f3dfbfed375e8772abeec6ba298c702e31e79be8ed53c9adb56f0efdb441c021dca7f63d837be3e9e296ec075a2bd15f599191313934df47d5933defaf3d30b67b293510013c339e39617e3e4abf69ffb4c6b8f8f9ff1d6ae4378fb8bf7f3939fbdfafe9b7d532c8a2873a7283fe5bfa5d5dde78a7e463d1a3fedfbdaf79fe3e7fca497d77ecb3f577ddf85b7bfb95d8f27367d5773fb8b39c5fab3ef33ad14fccd1dff3e77f76da3b948ebcfe388f213debe320a65d666829ff64df1ab7617db2d567d6bdedb560a4ba068aa2e23901cc1f049a456090cc3d68b690c4312584d32d05f3893ee3b63f465b793129e8dda2caec89294a00127115e7c78907f56538f2255db35f0807fed6f24a01c815c94784c17848801ed188cd1d7770b4a004804b26327412da9a5ffd7f29f36fe94e9915f0a23f88a430389659928343dbbc8088c64648cb4c8a452586034ffc144d922b51f6f8d2bf641048612931c3312303a6476a0dc20a1f1a2285c0184070b9a1d192b2d80f2cb0f95ce821344094a9066828a400c99b8a8f183a382d050890504033ab8388c1c9851e4f2b2297c1ec5e7cfae4c26d29fd35d6f695d63c5a7ade2f0c828103252f96909601518d16040a83504c65ef071bb096242450c9612666dc0f0574c4d4cc4e74b0e0a203f4c96debc783fe579d715a9fd786b6c6f6396286c6f73568d519c7162d3568cbb75292830b54a017f5c8d5f510d52b5efc5ddba94cffbbab0fbf4acb3eea4ebc2ee51dbee7a48af0d7adf97a5656e59916d651682ae34267743618d1ef939d7de6d35463fa6c472eb11ed094bd153697fe9b9bb9bf02e35e9772a7eed6f269a87c2728f3c5fd36a1ecd6d75fc74e24c1d8926e5bf34ec1b5f1fcf14c37425d1de8a7a4bcb6834e24ca72f6bc65b9f3c94e14c965818b06838670cf31c38f939fbcf7da675e6e367bcb56b12defee2fdfce467afbe9f66df148b62cbdc69cbc3fcb7b4baf4b9a29f51cfc5c37d5ffbfe73fc9c9ff4f2da87f9e7aaefc7f0f637b7ebf1c4a6ef6a6e7f31a7587ff67da67582bf19c14f2777778eeb7193d69fc7b1e521de7e82026b76468287fba658564b1ca702559fb8aa2aa88f1c3ff0532998ea0341d8ca6a04c1d45757b5cf655466390201286cd466d14506793b237a84f0403ec86fab380f1488421539bc7ea96fb4360020ea044d0760fb4aa32d0393412c92468bd2edeecb1346b039ade56eadfc7f2bffc5ee17292ef297a6856d2d444fe1662cdd1a6a2668a10cbbd92d70aa45e001b8eeda07d801a3d98bb7c6152bc1a1c66a05031b22f2677632c2c48d501088d50e96010690c448c11595a0414234641449b9f0010446096e7084888802f5068aae439aa2a0086583053ba1e37322f3b22c7a1ec5e6ef5c09d988fe9cee7a49eb122b3669b5a57373d3426086eb808e213d700849c223c8d00f151e4c7280161004789100804f6ac8c75f3335b1109b1070d0517ed8995ef8f07eaaf3ae3b9abd786b686f539240b4b731abc628cead588fceffc1665cee9415db6a1c2af5a956a8efc595f835d5a29a79afadc6a1bc93c781dca766dde154380ee41ea56d8e87f40aa1f7fd332533cb8eb63d6d055d69caadb6f2229ec6c7b9f66eab31fa3325981a4f684d588a76a5fde4c9dddd8477693fe9d9f79bae49bfd3f0675f33c94e2b2f7e1acdd7b29ac5725b0dde759b895ba049f9af0cfbc6d74533c511b852686f453d260605056533754fd68cb7ee1c3c711e4b30292f329c33869d1ef8f838fbcd6da675c7c5cf786bd724bcfdc3fbf9c7cf5e7d3fcdbe290e4356b9539683f92f6975e973453fa3271907f77dedfb4df173fed1c96b1fe69fabbe1fc3dbbfdcae451397be6bb9fdc39c62fdd9b799d60cfe2506ef3a77f76de34591e520deae92fa6676428e83fba6f8aca58bed0eabbe35ef6d73f95cbc70551da4bc51f4be57a9602edef7c15856e2f7a9bcba9a797ee23cbaef8cd1d36e024c366cd4e6d035926811e5f000fd9c14f2d36aeb5b047c87486bd27dbf6ec712969103251800e26da04500579b00fce1ff552d62179dbc3a1c01ea709488183fd836dca4f96e66379d50661242d944fe4c72b9888a456f4661538a9ab7c86eafeccd489d2b87942e280b11186eb3051ad563fd42426940f122c40320a5ce0f0f2a68c0d831803f7161a0a0c88c0015a62cf091c38882264ae82e3fc4e0a210e91c84561c6849700482880db98a121041a87460a129d0e12832a1946f9c78728f1fa32df028e573b35b6dca246cf7785a9811fa400a213d8038496262c612164304f02020c50583118690a7848c0f190902031f165aaaf082ba7ba4c081b4f8872953c35ffde68827698deab17e397f742acbe7188b4c36ce1f8db6dce4e630a813257f1299ef533745286b75d54ead19a764adaeefb7aa7d5bd5aa78efed9ef3b0cd42cc796fb56b3fd43ccd03b54d13bb2c0cad663dab59dc75e26ba0f8e96a077e6016669d58afe8811aae1704eba77e364dacdf776b0db710bc95ab98cbc06cebbc8c0bb32dc45c7def8339af6e1fd1bb9ef6ffa9dc177adc275ea0ebcdd1c65c2cd705ba9e3cfb35d3a384a2ecfa53a6898bb2a6e754ce505a0c276b65ad36168783c9588e4f3dcb7420b4f45c6ef271078bb09a213d587f534cfefbfceaa36963a84967be45fa573f7ff139a57d6b8a4773df0bbb595b924a90b0d96c2c8ee74d96ef5e7a2828b5b9884568d022b339df612744f963fd258783a9dee5f1be8f3be7a3b0f347378fa10983b1ed6c36168782f89f96c442d4d2fee3a66b73df2b3a75534441fcb52997d8734b3e2737d60ce5709d2a5ff040415d9b3b65298b4967de4914743e0ad33bf76b4b6d79b7acaffbea05adc65511d7ee3d2ffc6ad7851b88bbceab1654559ada5ca4b4f34da6b29557427432f34b1299ac8435a00020b570e17f5a98d7112e00445a216269c11226b4e2268c2811cf0d4a337d4559b8acc004311940443345628418b51ab67c0952da813336f3c9885899848961e09431f34f3450de733dab08980bbd84801f0ba0a68dd7001ce23ff030511a46c81d5725c530c2ae0cc0b64b2172d40a392b95735703178a639a53d950d798120b181280aa18a28722039e117f341a010020d67e32010050020ac7060a2108c36fc51294151800e2ed9f4600759b003ca5b4db18db01cce7293ebbd6e1a15099f35953ecb5479036b4e9ac329310ca26f26792df984a0ce728dd04081ab4c8eecd959d48a862c696aeeac40fb0015a333628a8c73a466462d98140810b032a54bc1605463744606648446782858e084b02c09849702859f1d0c00988bf5163309c08e1230b551514320691898b50826ac245e54363258715619465a07871874fa329f026e373b25b4dca226c7758da19121c3868047a8858822419f203c91321467814793142050848c381013616acdc541e37733421c6747728a1832880ff772237fcd54f92789146413dd631e5cb94c6f139c621528cf265423b6e71f319138a917f89cc77a993215395b2a887d24f6754a5acee338a7519c5a8b655eeb39ac53e9ea5dedfab7519a5156f9fca79187f06e27bb58a819d87418b61f8f3327bb5c7b6aff4d3616bb94b2d48378a691fadc3c0c7b0ccfb8fe78178d3b68cab15e30cd33e1eceaaf6d5a35a077299fdb798c3f7a3b5fbb8da8116c87a32b434568b6581ac17cb6ed9d418a518bbfe1349e2a18cd2f3296718ad855295aa529caad3b96a2cc7a59e6b3c2e303d8f5b7cecb91ea46cc497ab3f8926ff7d6ef590b43493a433dfe0fcab9f3ff87ccebef5c421b9af75ddacad382140e0703855e7e54996ef3ebe4cd06973f07ac000a66673be3f8f83c91feb1f3b9d0bf5171eeffbd853bee92a5fe6e6f0236730b61e0ea7ea4c10ffd28a97836967ff90d335b9ef0d953a199a20fe9a94c7ebb9159f8b9cca6676ea86ca153e4c50d7e43ed9d942d29977d104956fbaf4cefdda515bdeabaadb3a7a3d8b550a62ba3dc77d1dddb62ff3f0b671d47a283a4f9b837376bec553ae720a882e667e452090940fc61100a21517f89776e66704d7763986cb7dc5b86eef547100305b28aea4a42f58213c0e36148c60792eac3d90e462732022030258524bfeafe53f6df049d2233f044f0d02f269bd25dc6030b078d83484c40deb4966df202f332e7b6d77a39a8bd6a6151baa50a3a32d4cb89410ed0c29ba613200470ad188bc618840ebe7030928084d9069e680d09212361d4052d8080a519d8880981e3f40274824c033141dee0bcf32aecac2c7cd5bfaccd2e024d9f1f9d4c26a4ba4b7b4d11c9e9e1d9c1c262d05fea542888886b7634657a4868391141938451e60204159104348bb666c62e12d1fe89800b0bf6427050fad9f3cdeb2a39a8bd6c6e637246c85cd6f4a9aaf79714ecc27630751f125ff64c5b85bc78202532a16d04595e814d52055fb5cdcad63f94c5fb7eafc2ce9ac53e9ba55e7a6edbb1e924b447eebb213865975f4555627645a4372b79517d134ba8eadf66bbea6cf7830b79eb096afd33c79ef30537777e9bef70e6bdcef656792ff197acd6f259a69e5a54d63e99946b1487ecdd0a71357ea0c6412764bbbfdd7b6589e1742e785d552b363624e4e4eb8d2c96114dfcfa75ec92e1e3d18052fb48bf17d991cf4e83aefe597279db5e8f87eee4cbadf1e5ac73e3a6edaef356fcf1b862cf24f56afb05b58cd9e636a3a364d32bd7aebb9df4bd131f6b161b6faab1dd36c3dc5d04e5ae8d3a9bb9be37aba94b3635164f5ea7eaba0c05a8d70a1576fcf2ba39ebdf5439a2dc5cf712ea0cb17aa680bea1bc50ff454eae5f281e08b814a04c1d44755b5af65178fdd8faf29ab1c4a000d6f561cb6461f085c231e477e9812b0db688ebb4431207cc7efb46ff2d0d238001002f900a82e37d0035c025b282c5b992362a593182ce584181ea2b1904eff1f772c5631c29588080e1b8b366c9f5e5b9170cb344366fe20203a412c28db1fb4272f8c2ba6e1c4551088642cf0b18971c361e4039222a834025ca168fd1490f170c1a3d930e3cb1092e3009048908b0642330c204048083930f1c848680878054036d30a9d941afbb3f3283e3fe7b6e41dfa73baeb85591759f159ab2d1c033d5ea0213c1d12c8a8b1521aea51c17233e0464f89161621b8133c6c985835135ec302131bf1a9328302c8ff92a5323fde4f71de753f684f5e98dac798b00fb58f39abc628ceadd88eceaf63333677ca8a71b70e55c317512562ada5b1e424a668610d77eb4ae11776aaeed3b3e674235da7ea1eadedce36aab1a0f77d590acb2cfbb1adcc46d0d5c5e46e242bf22b7fe6dabbadc6e8b3782737db68cf578a82de7ed8e7ee4ec2dbdb0ff3ecfb4fd7a3dfe9f8b4bf91661fc9cabff27c3dab9934b7a5e020c8913a067a94ffceb06f7c9d3c53bcd279a3bd157514149148c491c087d58cb7065d25c3b9f44e4456339c337e7d0fbcfc99fde73ed39ae3e467bcb5eb11defee3fdfce567afbedf65df148f23cbdc29cb55f92fcc6aef73453fa35f8a4ff273fed261d7fecb3f577d3f86b7bfb95d9327367d5773fb8f39c5fab3ef33ad13fc4d090e82eeee1c677392d69f4992e52abc7dc444a4590834aeda37c5b2ea5d6cf7731c4a448563aa96988425198a7f3abd50a128be4c2952144f614dd14297e15cbaef8c5199e50804a8b0519b475789c306468b97211db697fcb5da7a114da4423090fdc612951d1800e22da047f0769b002c9c1a5d3f5c919ecbc5267d8be9c1dafbc96d942b2c1c459736b4e984924b134822f267129daa989d78d819d2d087089d508aec6edb9430d25972c20f7e1af72a45142aaccfe3cc49557fd81cc0b144e706670952240a362581a9404b0e8f4b07293c9c98601123040b03044af49871e4c9091b1d243a0d970a2e3e0e2d19923459a042c4901a3b9d0514a58006357a47e7c92dfecd088143299f9bfd8e287dca778be73579200260470205104c4a2a5aac3012e303dcb042a024031980537787c9c6a9671a68cd831915dea2830ba4873fb832293c1edff4f0a75185f5799c399f265596cf732ea1609c4f23de72939b9b543a25ff0f9aef54374baaab85d550ad1aa8e66a61f6b72ab25b15d57bbbaeb3b9e6efc0076df78dfe2bce58e3acfdda618bb7d1f6a0bd9d05ab0d479ec77dfd72a77518cc8f3bafd6d0d3b66aad267a6bb98cff7f147a5a97459a26e2bef0ab5f7ed1779fc771def616bc7fb591c59bf6d5bb55ce821f67438c84bd29de1b6c05c348d89367c77268d48c28fbf82b13c54b5aa5eb959e50de98d4d5ba5a9f0b04ca702ecfa9ae71420c5aba2e37f93994bdb4724896ac3fea463f7e8e75d1bc3714356abe43fa787cfee1734afb1e6916cd8db10cebf1921406e6f3f95c2096475dbe7bc94249abcdc3ec45410bcee67c832117ca3f8f5f824019eb653e6ffc3974be2a3b9f066b11344d39dfd0e773812889ff79c9cc855eda5ffcf4d1dc189752dd2c5112ff11a5cbecf9259f939f2b8782388fa52360a0a43e9a7b75e90a6ad4bc9f28e97c55366eddf12dc7cbfb7559cf563cba22ae86b97aff7da0ad9e076ea3ec795fbd2356a5abcd434a3bdfe44a5fda63d349cdf12914e2015f2009704a21c3ffbca697112e00445a216269c11226b4e2268c2811cf0d4a337d4559b8acc004311940443345628418b51ab67c0952da813336f3c9885899848961e09431f34f3450de733dab08980bbd84801f0ba0a68dd7001ce23ff030511a46c81d5725c530c2ae0cc0b64b2172d40a392b95735703178a639a53d950d798120b181280aa18a2872203163aa2741a010020c4b16bee48e094380068c50d8880735c862c31000ce6346f848908f5054061c2692636acf47f95ffb4ed27494ff9252507a9018059d874b478b7250b366a6e70ec938d844b56c8c8243f09d1029d998b76e58aa544181880653e05493c0735427282b4a3a4c69b2099f101018bc9e8480e0b9c203f5cc4704cb070a0315336b0d162c48d1eab92140b12c88001b65ef0f040c4e3e3e555639070f3965e63296023d9f1f9d4c2aa4ba4b7a4511608159f2121841038b1d101a5800353aab300a18898a303878a52f48004e9ac6881a15f30ab58784b092d13007650765ef0d0fa09c25db63373d1ae683e43c260d07ca6a4f99a17b3622432f610155ff24f556cab71147452c86232917c64024bdf37f35e5b8d1b799dc79d383f4b5ac3a570dc897393f6dccde75522bf75d909c3aadaf92aab3499c6905b4d052576a3b76cb55ff3351d0694aadd84b5044f53047a8775dddda1fba07758e37e2f3b7ffccfd0675e0bd9742aa8eec6d2b30dc522f975438b445b883bc91fecd6e6f65fdb6279de091c28ac969aede2e2f178b690c86114dfcfa23ec92e1e412906289b8bf105bb083d7aebbdfcf2a4352d3abe9f3b7feeb787d6b18f8e9bf67bccdbf386a18afc53d527ec165633c831351d9bdd4b9fde7aeef752748c7d6c98ad0eb6639aadc71ada490c2d1275776fdb4d87aa3eddef94956f562b64e8d3dbf3ca28c8defa21cd96e2dfb6d257f2421325ad78a3e87dded20296bcef031f98c4ef6bf1a869e6b5ec6575131080c39b1587ae91868502837320070a19ec34ca721a1fa763979dee8a70f348c01871002c8e03cf58b6c1f0232ef0fb88bc8540bdfc7cbbb46b1098c5b88dd2ff27f29fb6f994e9915f49d4e7d81c0476a1e982898b2d19c04c2899d6a72402b1e33d25cfa54902a2d98f5767f58ac20af2c3c00e09434634641844900c68315a42a1401204aa2153844cf89cca97113338904870e690e1135b01c200004a0acf8b034235270e6aa81062524404eb446cb369d91b418fe2f37f2e864c457f4e77bda55d8d159fb4aa8ac154e981b3c2c7a96445086b42a720088f05287e107970440794264f08e0a0012126f4d68dce4bc4a7851e5494df054b795ebc9fc67e7545b31faf0eed77ccd206ed77ceaa318ab3ea15a4f397b0199b3b4dbdb61af7bd88a7d1853c7d45a1de05069c7dadadc6b97cdec785dca767fde158382e0e88d44aa1f77d585ae694156d0bb31b74cd31b75acbcce891df73eddd5663f46f3a9a1a90684f578a9abafda5e7ee5ec2bbdb5f7af6fda76bd2ef54fcd9d74a32af65c63df27c2dab7934b7fde026d356e21a6852fe2bc3bef1f5f14cf105ae13edada8d7d48c46a3ad64fab266bcb5c94318ce64471361468673c62eaf8293dfb3ffdc675a7f7cfc8cb7764dc2db5fbc9f9ffcecd5f7e7ec9b62514c993b4d7998ff9656779f2bfa19f56c3cdcf7b5ef3fc7cff9492faf7d977faefafe0b6f7f73bb1e4f6cfaaee6f617738af567df675a59fe66073799dcddb70dc84b5a7f1ec79487783b4b0c38b3381e3cdc37c5b0da5d6cb758f5ad796f1b0a447da24c7d89f9c8f103ff7472a13e1074c1c88c2078faaaccec731885d94d4000101bb5595c913ebc10319ab8604166c84fabaaec9e4b6cd67cc36df249f0b22800c8f845a3e90ecbd0dd8788afd139e768a349efe35e6feb5a9800462b93ffb7a3c949f4286221824018640c092d834038dc37be4cf49ee66c341598dd06233717ad0b4683e9d8e1ea89fd38d13063156453b6184201933310c4c3881f211b1ea0182d0142427c747c58d450d1b3e006884f84c381c2c3c64ba7c80002231f22867e7c2c0dc94bac9ed871f3966eb311f090ecf87c6a671526d25be2a8cc8d45d0f362858b1c343a94b8f1014681a00344423078b5584c1c48d45419094a524c3a16e3a285b7849931036097d54e0b1e5a3f6f1c66476e2e5a17ce5fe40c08e7af92e66b5e2c43dbc9d837547cc93f59b42ee789279a923a89628a6a4c9d54a3ea06c6ba9ce7002c81decaf3b3a436cfc4f3569e9bb8f77246319efcd66be70cb3eac8d75aad90698bec72272b6269f4195bedd77c4d8ff95a7239612d65a769fade67a5ee6ed2fdef7dd6b8dfcbce23ff33f49be74838a593952e8da5671c8a45f22b8636993a92a7208fb05b9cdb7f6d8be57921785f582d35dbc5452412752493cf28be9f4dbdaa5d3c7e2d0d56702ec6575662e9d167decb2f4f6a6bd1f1fddc795bdb59cd9f4b2f67b6baac1dd36c9dad69272fb4c9d4dddd75394dcad9b128b27a75bf4d5050b72a81a6576fcf5ba39fdf752a28153054d1130a388a20ca5329990a8842c91ea844142a0552d50decdac563f7e36bd66a470220c39b1587b091652be9610208857302761c95f1146911183be47ef2d16fc645f074ab00a40598ebc6e079f0d347d366df862e3017bd508dd657c36da86d889b88d870598c6df253baa0c332605c629c047db86d4952ae10d13e3746e6fc0a9f9cbac18b0228a5bf244b88543590191107520ad07450f141401125475a00185d283855882e00c141d32346900a4e02ccbc943e9698d051bd603220c9822135373e6cb8316264f203c58a06207cd0382788f08c992fb3f830a4055d63be3eb793210e5dcfa2c95d420ce091438666c21331ac49c0c20885f102a2c90a38f21052c280008ba025c50e245454818564b348e1a524a0bf8ee786b7e49f213e44a294fe922ca66f1999e6eb7b41a10cd3b7886ee6326b17d28ca95f04d679b44f10893a5557e7ba308e883a55de6b57e66957767575d3eae59ee6d9ace6c13a2cc39cfbfb1be816e3fe6d97bde6eed13cccc3619b6b99bb7bff37b3585731f7e7ea754fc33aed3d5bb5d693ddece3b60ecb1eac1df7619f56eb877d9bf61af76dce79ee779c5737f7b0aa69d7e6d56ef3c0ea53fd293a4ce5a0aa3ed597a65dc9d810913037f9e3196a1046d978c41a4cc730a24ed469437138568ceb3bda38a6f300c2c6662eff76ac08279952c5da1fc2e0b7afab2d9e0e434312eb0cd2b7e4eb075f53ba3349af78665b5916935e520912369b0dc551f9d075dea60afd8c59835604061062b2d6b97656a8fe4bbec9e158a5cff037dbbf1dd3932cd3b7582cd6d3e55eef6c3614877ef49bbcb456a8d3fde2669367b61634da27887ef4932136add75ebe2e37940ce56c5c095fe0403f9b3cf3e8d4414862bd43f4637a924566bcad9ba4eb4d511ee75df6f36cbbc07b71df75d5bb38ae6adfe5b8eef2af74d1316b90d2ad7339620aa778d825d636041249a9319200089dc8a0dfe42e5f2386df0c9ccac5b7e0b23801e8c71440829c6129ace5c929ea31083950ef8fa0a754234ef2000caab528a5249249433a57eeefd61021003270eec310ad008d48e526d0253fa59edb8d29722b507eba136a0120da8b36c615b3017be47841b279f8f09ca07085463b7c8c34724c34e831e0caa9d1e04322e2c7891f1d12203ce0187043c2e8e8760d10ce829d564d091c1b801d970b07042107585e5585cd9fd77c1a2b01f7e4c75b534b569748af59a3ac9920230ac00404fe868e8d11a318aa002e4c423694fcb064a4c09009c162c60810d15f2d31b1f09a105a27083f4cb6297c687fcfbccb82682fda98dacb94648adacb9834dff36256cc26e3ef50f12df55ec5b85b378e2108fe9b50c0131414089e68df8bbb75271fe9eb52dd6f93d274265d97eafeace9ce167add646d5fb649bcaa205d657542a62f25771ba944d2f82d5badaef99edfe2a9dc6c613561fb2c79fa4992bbbbe86a4f3fe9d8f59b9e43af77f8b4bf8966a491ca49a3f97946b158ea8ac14b254ed431c821fc76765d5feba2b96f84ce0baba5a7afac88888870a2d293145f9d4b9e925d3c7a2a25aad9c5f8c2480f7cfc967e539b9bd2b8f8f8eaec3974b57f681ffff8d8a9eb7fd176df305c957aaf3c85df92357b8fe9f9f824b1784adbcfaedf141fe31f9db4f561fe9866fbb1abfd4bed59346fe99a96da3fc4fbe6c7aecd4d2ff89715bc547277e7389b8b727e2c8a2b4f5ded2629265a75b1e0296df79551cfdeaa439a2dc59ae34ea6d317a2a883946f143fd38320ecf4994c3014946832811f45d13e975d3cba6b7c4f59e50a08c8a0cf8a43d7f8a4151f563c9e1b18fc35cafa16014f73a227b97da66ec646f074db00ac6b692b5723aa622d6f7e34f8898a0e44612e0fc8b62e4710da30484870535a771a911759508084b01831055f2d468dd1d2f6d2a6a0f87dd82a8698990b1d2f642826a8bfda6554ca70c262424b8c1f026890e0fd40a64245c30d1da120362fc0d31224282d8487157db849a11c66c414a9b132e426048102260c8040c12183a5858d5204203725601c69009353d87cb84d2fa321f024e2f3b1570de6d1addb94aaa298cf000537227aa4584181a0a124eea8a0c4c7e7a108111a971c4200e01830ce485122611eb894ec3639b01006e0ff9d460a6ff58f313fd226a8bfda25f52f24517c7e6f084491fa17531577b8b9a86423f2df90f926f53154aa6aace999937660aa1aabfb6c625d36b1793d8c6253fb6cfdd4efc3b44da31cd628bd735e0e7b1f9c711957e776b30c6f9ff55ea377c393a3155b9c6518e630ecc39b562fc751faf13eacabdfe428b5de37e953eae10d63f493512de33a4aa9ed3e4ecbe8b499ad98dea9751fa775d802593f9aaa8c95c0b240d687a96ed1b880e92076fda711e4a16cd2f3294f10ab04525553d570aa4ee79aa9fc927a9ef1a42ce859dce15fcfc5a0862654b9fa8332f9ed73ab9bc62a23419df9c6f2ad7efef8b9947d6b794de3b6d665b3aea16461e170385547e5c1ca77175524e8b439be18205830b339df9f6745f2bffac54ee7427dcbdf6dff7a525fba52ff62b3e91b45f7560f87537524887faa86d78aacb2bf89d3f5b8ad0d91fa189220fe1acce2f5bc86cf434e45233bda86ca112e4850d7e33e559900d499f79104a5be74e99dbbada2aebc5755b775d37a15d326be737b8efbbab96d5fe6dd6de366f550539e36c75276bec353aef209861e666e47f8c7c977c208ca68440bff5415fd8a5c8532e5c37bb06cf149e0d038009181d4a496c43a18ceb00ef028b658c6508e78988a521e9a5dac38d521fd3fca7fdaea458a8b644d4002c437cef7d039fa8eef12d11290710ce4299202ac10240809146c031e350fad0b6c26838423b2803340b850bc466e143870bbe9609302028b220e473f4144361e5a30381b62dc94600d29305a2530319e701019a07183334452d3c2f4565ef8a438188112ab3f3d6eded275ec0ad6911d9f4fed586121bda58dcae488a001128a31aaed20a0e2c4d0bde4b06153048384e024b4e0a885c14b508815023aa6e232036f99326306c02ea39d310e5a3f731c663d6a1e5a179bc390e3099bc394345ff36299594fc69ea1e24bfef99a71b70e54792423222ad54769c807b658352fc6ddba100fe575aececf92ea74a7ae73756edabee321c534e4b74e3b47fcaa1e5f699542a62d247703290951a2cfd86abfe66bba4ac9e4c603d652769aa9d2fb88eaee36dd2fbd8f8dfbbdec4cf23f41aff9cdb48302296994587adea13824bfd6742ac599ba069984ddeedcfe6b3b2ccf2ba12b81d552b34f4e3e3e3e3853ca478aefe754bb68178b259307253b17e32b434568d167decb2f4faad3a1e3fbb933e97e3b681dbbe8b869bfb7bc3d2f08bec83f5fed44a1b4eb6aa19da4e954aabb9be378da94b3e3307cb5eb7e9f545fadb25468d7dbf3d268d6c7f2c01615517962e87dbe5ac958def7c9465ae1f7ad3cdaaa0600b079b3621026ba405d2051c3e7833801bb8dca38505d3a7081dff0cce712b6b304251900b2aa9345f0741c0018a68879d4b437785eb391311d5dc5a694de79b2ed88a636498d92ffb7f29f1abe487191a6263ef106205a1cd4156096d246d149b659e14d6aa806de607e051c28282c0f2de994f216a722339ec711ca5452c8f4301080e5cad1c32422e1295980a0378c7081c2490d1b190ad492878f1c58e6408b8cc800809199242362100932a3c2648087c5931654a561c2cd5b7a8cc5809764c7e7533bd653486fd9a22598141d4342114d563c90991d47402f12f4a7f9e12263858023303082905e5825218dba404a81b7ac603253b07faab38383d64f183f5914968796d4f217729cd1f29792e66b5e5c4a99c8d84b547cc93f4729ec95ad84384f28f478becf0a8540c38ec5a1b0570681dbb8cccbfc2c694ce620cbbccccdd667ae0dea447eebaa73c4a38af255553964ea4262af072ce126bac956fb355fd32f68145e2eb096df6976b4f771ebeee6dcd7dec7c6fd5e76def89fa0b3fcc5596d0f587a134bcf2b8a43f26b88ee74304ea6206fb0dbd5edbfb6c3f2bc16320dac969a2d1269341a8cd3f191e2fbb9d39eea6251a3d080657531bedf36a14537bd975f9e34a643c7f773e7cd77797b5e101c917f8edac36ec79a35c7d4746c6e2dedbdf5dcef65e818bbd8a3adfeb5639aada720da490fdde974776398ab39393b0ec3517bf7db81848e55337c68efed795554b3b73e48b3a5f8310cd4813830442348e0c490ebfcf3f9405cd77d2ba1b0eb3e1c0db1b8565d2c763fbea6aa6204028878b362f024fa6643854843724203f6162df98d792f0c00f97ee7e66ff0f0b22800609fe7f7297a1e16dffbbe4576dec3f7129d13e1a34814ec814a920825ff6fd37f26cf2792fc27de7a43d60c375c542257f71e2912214638ba166e4c52733a3c719b766e0c2336a7b606468351949f0f2d2564504a3a08d1040d095d183e280abcf881414506cc26a8c540102c02810f1c8b930138760810c252e3a5470b02295cdc5891f2b30117348386c70bc3c4ea891e27efe83b160206921d973fedacc2e8bce36ddee82400ba605bc97c160c012132e427800d1ca01031409490984003c723698d20c9c9d1b1981a1a7847072f9204bbac564a70d07aa9e3303b62736a6b6e6e23ce4cdcdc669cf99217dfd07a32f60c155ff14b17adcb79327a625135f8be157a0a43faa56c5faccb792b5fe9f3589e97e3dcf1523c8fe539797b0f278a71c86fbd56ceb0ab8e7cadd50879ce885d4ea5454ba1bf6cb55ff3253d66e492c381759495a469f43e2b757793ee8fde678dfb7dec2cf22f41b7798ec453526975291c3df34c4cc5af16da64ea48de812cc26e796effb54dc7f23ef04660b593ec97171313938e64f2d9c4f7b3a959b58bc3918b8216cfc5f8ca4a2d1dfaeb7dfcb19c3b4d1ddfcf9d45f7db41ebd843c73dfb7de66d7941d0257ee96a16763bab79e478928ec9124cb3de7aeef7913ac61ef6cc5697b5e399add3eeb78bdf998e57ec9fe2b783b8bcd971ff584e9c7691429b4cdddd5d876b52ce8e297535eb7ea79c50b62a53a1596fcb5b9b237beb8333db89bfeb5428d507ae26cbe90be987f2544aa6fa502819831545a152df5cd9beae5d1c763fbe64ad762700729eac1884854534e8b079e5e68106fb6ddeb88a3c1d68c5a2f08f6df224d091b80666e87c0e00189828098022228110bd7e3ca6d94bef1942ba25ff1fd37f96f313493ee98b205f83291d8e82d9a5fa4ec9a4603fdac7440e4f3d71f3fa8672c60412a49b8b762656033254a0842b47098dd008239c143750cc306dc030e0c7a8830e122d04385e3660641ce12e30687109a0d2a40b244464c245117515911c05e9f183ab0084f3e266c825ab2b7adcbca5eb5808d847767c3eb5b41a13e92d71d42627c582cc0e1e0660443f5a35bc0f35243210040ee48260001809a221c2e78487538c96bdccd4c25b3a809921d867b6b3c643eb678ec72ce9e6a29dc1390d495381739a92e66b5e6c53ebc9d833547cc93f5fb52ee7a980ae02e96a35a2ac54a1ab56b74fd6e5bc051feaf3589e9f25d5f14c3c8fe5b9897b8fa724f3c96fdd76d2f0ab92beda6a844c63c82e77d21251a3c36cb55ff335fdc5d492e3096b393bcd94e99d86eaee3edd37bdd31af77bd9b9e47f867ef3dc690775d26ad4587adea15824bfde742ad59dbc915cc26e777de099c26aa9d92e2e222222dd29e5348aefe754b36c178fa69692d6cec5f8ce500d7a74d82ef717f9e7ab59d82dad6693636a3a3651ae66bdf5dcefa5e818fbd8345b7dd68e69b65ebbdf4e7e67b1bc643f25bf3dc4e7cd8efbcb9352682725742ad5dddd753c7dcad9b128be9a75bf4d52c05b8599d0acb7e7b55193df752b70f5852c5495f28de207facaca6cf581e00c854504c1958fb2dcbeb6a6ad76024270deac388c8dad2101871621148432d871d4c659ea0b9cdd897ef2f66ef1e4c902034142665993e596ab2c6238b7009f88015c9a9e80dd504b6ac9ff23f92f48bf48719116ace92b62d1067c3166a5055c63a1aa41569821ad4c2af63203c4288a512633072dab05a3b1c2e202e587438e0d0b387a762a6c60897092012e4b907c2851c467c70d22077c4e7440b100c30f203c621a0628478b1ab423aa204a744a11446057091095cf901e95abaac8e0e6253d661b8ab505d24bd2e8cd4e676360e6674308164252283a7e7ac8a06142068407d58b113a2a7ccc8841c0071212ed9261c1be4bc6e09800b0bfca13827fd6cf1d6f599399839645f39a514c41f31a92e66b5e7c03cb90b15fa8f88e7fae605b8d7b8124969302cf5b014961087aa699e7da6adc8ad7791c8af393a4312e85e3509c9bb4e778402e0df9ad97a78857d5e46b596d32a519b79a4a0aec42c7b1d57ecdd77499d04b8de7abe4eb3447a177b1ebee16dd0fbd8b8dfb9dec0cf23f3f9f794d24eb5452dd85a46719c5e0f875428f469b8843c920ec5676fbaf6d903c2f032ef4554bcd8681393939d944231729be9f478d2a2f0e432f0852b28bf17d752e1d3ace3bf9e449630d3abe9f3b83eeb77fd6b1878e9bf63bcddbf37edf6afc73d528ec56ac39e4989a8ecd4ed55fb084f631a747a3ee8185b801390a0c000044104000110001420712c08348482114830140986106000100c20c1641bca2f0662259123a51ea024500f44400533982437513b5972a65f1fa50d1232a3a5d82d7bf0518b4751c5964d39f34dc27923a820e57361139b97cb588c892f65265ac5d9f409a6f01c70a9452f5edacc15fe8fa94a2b5ad469dbe4423348b6a44cfc8af4560fb02ffd36399e5231c4afdc44804c7defd47b9a1c89ea1496c3b752fd468adff8ac8c3b0a7aefef60a1d2dc8edb24a4ea6ef76e403a8df4a8c3a19d88dda4f81b176edc3dfb102adfa65d245f9d9aa8fd47032dda7087ed0cdf9d37d557d2f63b3b8fd38288bd743a4b7228f5677e42ca57692a7c9882c3c981da9a44c433c5b8cd9b2c426f4af4266f3da01e5454845c3d48af160fc7c5088ea8d1f4962da4b95b0747da0e5e12ceaf9d1da2e4790bb2ff54d0f9a6a295abfbf16f2c9f4973c64f95cae60c439346634ca2a239a932cdec30f3a3bda326183426f1eac279e1ba120ad63d925db6fd2c4d6154fda572be724a35e0424885bf57d7abeeed66a6290a3cae15c0a20b5079c421d7f8657979ec0025c8488f21a9be2e3f0f896c03f1dddfc5162c0f1d0c5d5c869418ffcc31e688793883f1997b3a750501bd0ba8a3540e181b1eee61990b69acde63d08b998057a6a79a4c12f3abf5fae798d2b14af9c22b40dc44e4bbc0696bab2435d6c243fd66eef422080fc9f416b69a36ed5f20eb2b3ebbdd7131ec8a1e95e09baa7566d09a9bac20bddfed776fdc53223d7cfdbb8d464feece5e65ad9bcfb52980bdd9305dae4e107336f42db8f03b175ed5569a0407fa4c4ee294461d107436eebbeced52fb01faef14f1a5aee49a87ab9d08f62d34a748937c8d85f0b8db53b2f665a5589b3a1e8ba97011ccd25d323050a78d0d32817bb9e473172ad058c0763b1bd0f5b7711fe3da4852ab1895b599994419fafd350229748df09f1846aa96c08f328ea9b9e55a6213c56524424ed1b9dca7e7c5cfee5ff5daef2e8bce578944ef6ae9682cd5ae66c28a58c63bb59e4f96757490ba5215213c8fb7a965276ee538b309482374fa0368b51e516b570aed0aac0f7616d29ae6fa383d503056a4a25fd029da0edc50bcfa73e8452b14590aa722190ab2a4244f5af72b9617ba741232de864b7043173ab5380b1bb3bb2783711c0d255fea1025dee4c2a73677b951690ff998e65eeac2be71249e74a1e23ef05a740131b73db7ab878e32d5765f16a1479b2407fc89568a0ae937d4b2e2b5ee7ca6f599a4db33e90674f195aee21d46deede97e9fa77b9e81288b58fb5fb092d9e12288a8cdb0c97ea4a22adc6bb69f352984e27b701cfd18b9715020cb8029c0f05ee6de370d5a8fb9d422acdea4c849d4a27ef6ba12c242f04bd929b4caf93572abd12b480a592c9a32d33afcb8bc3eec7d72ceb26200085372bfe5ae12a56e08789200f4e07d869f4c657cbc0f633b17cf19e6dedd4ddc5a418021176812ccc287e3832728600d0e7b0431c4a29d3b13d92ff47f94fad5fa4b8483740591a3047f555528bd7c0f6795908c808cb85ec080ae155b9eae016a2998716c605bbd11a9d363e46b058cc401122b6001a7b064c204123652800286a9da05042008b819007956410210368e885a7864f89e736297e4000395e8860c0014285470c0c9657e5b0e3e62d1dc72ac036b2e3f3a91dab2ba4b7a451999b0b10787a42cc2c0479b01f2d5f071753ca89a6e6c7440b64c548850822901c0dfd6a818181b74430637660ff62670307ad9f37eeb2a299871686e62d72dc40f35649f3352f9681ed64ec39155ff24f166cab71f47312f0804a45e223aa2fe579b3eeb5d5b891eed4712bcecf92e270211cb7e2dca43da7437a49c86f3d768e9855455f633526d31772ab89a08427d1676cb55ff335bda5a452d3016bf99d26aaf43e9ebabb4df74bef63e37e2f3b93fc4fd0675e33c94e22287d124bcf328a43f2ab8346a13613472093b05bd9edbfb6c3f2bc09b812582d357b6565341a6d26948f14dfcfa85ec52e164b2a06506417e3fb9d525af499f7f2cb93e274e8f87eee4cbadf0e5ac72e3a6edaef2f6fcf0b822cf24f56afb0dbb1e692636a3a364f2cbd7aebb9dfcbd03176b1475bfd6bc7345b87dd6f27bf73585eb29f92df0ee2f366c7fde5496dda499a46a1babbb74da74d393b0e4356afee77888937ab2e35bd7a7bde182dd95b1fa4d952fcdb96f2521da8a224269d18769e9f9c7ca9cef3be1255e879271d55cdba8e66ac6e1e1d0fdeac1874892e0abc443e4231cc05d86954c63f3b941e0b967fed8370eaac7ffd9b2fb1a1633b06e7c0640e34577c60e4ddb264997888ed9dfcafe53f357c21d25f5ac71e97f6e5810ba81519416159d25718c9744de396e8596d503841711e032031a7d6946a95443c54071c3d4a37601cbe92d407181d06667eb4c01c14191b9400889994088287966e068d153a5c5080e123c58b971918009c480d93510516085025207025112aaa8a42839377f49765c13db2e3f2a765d5149d7794cdd3850c0506c0500e7587090e8a8097871c2146014898b8948818213c488d05d3039656ad985ae01d4b283204fbca553e70d07a79c1531624e6d49a64ee22b26464ee32ce7cc98b4f2d0d193b868aaff825a9a5cdb615f5988416785e09f58421f540314fa5cdb612aff3b66ff3729caf6d64dbbecd49d96f311bd586fcd65d250b932ac857577dc9b345d4662427b40b1d65abfd9a2fe92bdc681603d67155921dee9dd5757773ee73efacc6fd3e76def897a0c77cc681752427dd85a367d8c454fc4aa13b1d8db31dc81bec1676fbaf6d3a9697c1c681d54eb25352341a8dc6e9386be2fbb9d39febe2901b2538815d8cefaa43e9d051efe38fe57c35757c3f77dedc6f07ad630f1df7ecf796b7e5054192f825a93fec965533e778928ec94ea5cdb2d557ad7342bb18a13b9dee6e4d8b694ece8e2925f577bf473c9f586d20754dcedefae0cc76e2d7b4d027e481a269e2f142ea7d1c045a85bccf67a540443f1f90374531af5d1787dd8f2fe9aa9a484885272b0653e188930c093132275c0eb0cbe6c95504f634ce84bee9ab49f0b22800cc1b75b28ae930f84471262364070db7ebe96cbe14e67fe4d00a21dfffdbf49fc9f389249ff4880eabeda9006018056088391112d1d1b4780c11834402d8f763bbdce5800222350f2bab15ab3160848516404e2aacc8e04245058d99d551727752a4f4f0900418016102b401838405060605467c0cf0bce020a3a5465e41c03b106388cdad144282a16192e0860917172db1e3a5257d5661ae10ec37ffac30da0aa7256d732647158404d3a124878809e2d8a1bae1787c50a120a1e1c4490f96cfa869c5c009d1ae17560cb4a4039a52c0f5972c5370b07acef15615a979585936971961246c2e434e6c4b7b67623bf8fa067aedf87915e36e9d288226937b69c5a4b28232994eb5cfc5dd3a95cffbba54e7999cb30ea5eb529d97b6ef74402e0ff8abcb32ecaea8c853199d8067ccc8dd5254a1273a4da5f529b6a5bf84586e3a20255fb91c85de615e77b7c87ee81dd6b7dfc9c620ff0c7acd6f229a97a26a4f241dd3e60dc7a7373d1a71a2ce0483ae579aedb7b54332db075d08a47596ddd2f2f97c38d1c861f3dac7a34ec9ec15432c0a54347baf7d790d5a749a77f2c93c671dfab58f1b83ecb783d5af8b7e7bf67bccd76c4170357e5e75ea7a85511cf23b4bbfa5e7d2a9af8efb9d0cfd5e171b56a9bfdaefc4d563f6dbc76f1c9276ec9fe3b783375becb79fccd3a67dbcd0a3517737c7e9b40863bf61b8ea94fd4621956a14424da7be662b9ba16ae98313d7799fe34ea5d307a2e602d227865fc94da6d7e92b955e2ba8b054327d1355fb5a66afd8fdd79632ca8d00c0f9925eb025ae66087cf4fc9090235cb7cd1947115c65308c97ee84ee00b128007ccffbf748f8adbb76d0d262c477474b3f085ab5851a19a0171b0591e4ff6d3d8881441509c7ab52ac454ec6181961b1371c8424d449a6fcf05e294dc6d91d3717ad0b8cc662c723754ab979f93001a2404e0b0a277a10fca44008ccc4e8d98199691dd080e33061038d2da607246354b4805e1e1d5294e7020c69e7e304a345a28523f3a312ab1f6edcbca5db6c0be3c88ecfa776566122bd258eba6878566410410370728344a3a58362848f0b1b06c41425786cae171e6266e8c8f0f93ab6e2420b6fa9e0650ac02eab9d0e3cb47ed238cc76dc5cb42e387f216739387f2eda4dc65ec9a275392f053a0aa42ad528a242858e52ddbe5897f34e5fe9f3569e9f25b579219eb7f2dcc4bd67138ae1e4b75e3b6798553bbed66a834c59c82e4732114ba3bf7d458492b3096b293b4d93e87d56eaee1edd17bdcf1af77bd939a8a64432e9d2587aaecab4c9d48dbc043984ddd6dcfe6b5b2ccfbbc01385d552b353524020503732f98ce2fbd9d4abdac5a30805c4a4e6627c65a5931efdf55e7e79525ba1cef2f6bc61c822ff64f50abb9dd52c6649a55711dac9076d327577779d4d8fac5eddef9011f0561940e8d5dbf3d6a8ef3a15a8fac2128a1af946f1039d8444a6fa40502652228220c9474b6e5fd72e1ebb1f5fb356bb010124bc5971081b7358a861f30987a1053b8eba3c44fe8b3d5490eefd6eedb23800c0349b20d10b52022a2c2005810025a05c100a050aa40b532ad26f2926ffd7f29f5abf107191ae63d0ef71e820b91a4ce31bb8485e85122e53ce33551589a303a2fec2b703219a8b36a61553097809c251c628219ac1a281879f13304037241a6c3c46e80e0f48101111250525180612c46e87c3a1c7d461837283a542c3878e08811e538bd2040a68c7450d16574dc1e3e62d3dc7bee0a0ecf87c6a61b525d25bd6280dce0814003f6c80b0325a5ef8f4b829391040c50c4660c710a610ce2c9063fc21d12e97985878cb961a3384fd253b2b7868fdc4f1960dd15cb43135972161286a2e53d27ccd8b69623c19fb868a2ff9272bc6dd3a1314985299802eaa44a7280829dae7e26e9dc977faba55e7674973ba92ae5b756ed6beb38d5c1df25b979d30ccaaa1afb23a93290cc9dd4c29e269f41a5bedd77c4d7721a9dc6c612d5fa7e991de61a7eeeed27dd23b34bf956627534a9fc6d2f38c6291fc9aa10cf208bb9ddd2ba123575644221157f21c46f1fdecf54a76f148522149995d8cefeb14a147af792fbf3c694ea30ef385d54ce68936cc567fc718da490bed79dddd1c67eb52ce8e4591d5abfb5d82026975c2855ebd3daf8c92ecad1fd26c297e8e3b014fbe50451da0be51fc404fa55e271f08be1ea844104c7d5445fb5ab2ca0d08b179b3e2b035b6cec88ecf87052105ec354ae32b471b8a56a07bed8d6cec10a43c000093848d4559091b9dd1e8a6a8a65cdf2800190cb250b653bb43fa7f94ffd4fa458a8bb4089b64c1d80639069e43175c970ebd748b5749c872738de3424c9e0609a2998756a6051350090c55e08189e816ba69b821d193e187e3c345c7e1c725860cc504081af4826181c7c967448b17a78d508a82ce099f4d48a7a5048fa843881418302c0db2e2aa403b6eded271ec0bae911d9f4fed585b21bd258dd2dc0c5891ba50a3c7c3eaa39b0074c3c6072e058a8705942b640648d0f841023424a35d2e3230f0962c352607f657ec64e0a0f5f3c65b5634f3d0cad07c861c7f683e603b197b840bb6d5b8039f9f3e8a428925a813e827d4cc736d358ec42b799c8af3b3a4389c08c7a9383769cfe58c5c12f25b8f9d236655d1d7585590690cb9d546566149e9526a39602d5fa769eadec7527737e97ef77de63592ac34b2ea92587a966dda64da485c481e61b7b2db7f6d87e5795bb80eac969aada2d2d1d1b1914c3e527c3f9b5a15bb58ec520cac6417e3fb2a2168d16bdecb2f4f8ad3a1e3fbb9f3e8c7b2c83f59ad3bc7d4746c96565af5d673bf97a163ec628f1d613b7dd04e366893a9bb7bdb729a94b3e33064b5ea7e8b987cb30ae3a0556fcf1ba39dbdf5419a2dc5bf6da80fe581297a32f1c4d0fbfce4e485f2beef55920abfefc4a3a999d7b18bc5eec7d78cd52d0000086f560cb64415055c221434589809b0d3288d6f0dae58859c7dec896ef04ad0d2380040f1d403e16f30d2899469e9a7a5481907729ab419036139b40a1563c4b80d3e497ae4ef6660be161507d764e059b4da335a2a9ef9a0c8c443de11c5d2b6b903a39a8bb6e59215c910faf1c10971234628f383008c434b059a8e0a0344303c76f0bc0082e1927d3d41077a10b53cd8c89022e4828e0f2d084e9030d494f049f5b8a286ed87e7a705cbab8ec819e0a2e84c8d8fd78e06a1024022220a102486ce3147ce001a2032337e5ce0f48c86563b4120fd7269c9c25bb6d09800b0c366a7030fad9f35eeb2a39a8bb665f3183226c2e63125cdd7bc78469693b15ba8f8927fb264dcad034bc294484e6282aa812feed691801ed8ad3a3f4b6ad38974ddaa73d3f6dd4de9a593dffaec8c61561d7d9d55994c5f48ee3692227aa3d3ba8b49e57613d612769a9fe93de675779fee9bde638dfbbdec5cf23f43aff9ed44f34652da1b4bcf348a45f2ab84fe3eeed485e47741670aaba566afac904824eef4798ce2fbf9ebd5ece2d1a492204f418f4ef35e7e79529b4bfde5ed79c39045fec9ead98f76cc5687cbfd7ddddd1c77d3a79c1d8b22ab57f75be40455ab0d20f4eaed7967739c0aa50243145a72028e2288f2540aa602512898098a8842a5408a52037b090830e1cd8a43d7c8c2a3d001c7869e1106bb8dce65e67f2ff37df0e06eea12b93d1900b26a9448e0d23600c0ac23840008858e408ef112886295173323b10364b9af1a5067592dfd3fcaffb5fb458a8bfcb2cabcf9c60f05bc034ca9492004085d064e6cef37f144a89b70b4a188021f310f6dcccaa581440b2095020a6389714468846c46079d4fa48342a84006c890e9e00c98f05c2e62c8c6060f23056ab4882ac7cf895297a2e66407b862e843f983038843108a557d76dcbca5e3d806b847767c3eb5635d85f496323a73833a5da03a18423c346c02bcd4b0e8a0b038c06365c1d441c067e75ee88189519bf512e3026fb980c604c1de7a9d101cb47edef8ca7ec43cb4313297214714329733ae9d8c1d43c597fc33e5d2661bcae7a58f9a4ca289a9047ac914f358da6c4be091bcedb4f959529c8d64db4e9b9bb2df7244ac0cf9adbfce11a7eac7d7577d90290ca9cd4a564292e834b6daaff99afec2b5cc72c05ab64eb3e3de475277f7e83eb3c8ff043de6b3118c54b2d224b1f40ca33824bf4ae8aed3469b481661b7b0196c1c582d35dbc5252424441b75777d7a5d2c722d08566017e3db22b1b4e834efe59727c5e9d0f1fddc5974bf1db48e5d74dcb4df61de9e170453e49fa93e61b763cd9c636a3a3649a83ede6ac7345b7723b49335dd75dddd9a96d3a39c1d8761aa4ff79be4e48b5507367d7a7bde17e5ecad0fd26c297e4d337d260f54a1a5134f0cbdcf53525a26effb5a262ae1f7a5785425e6f5eb62b1fbf1355f55130060c29b15832bf105a942cf0d103ca20abb8cceb88a65fd0039ae7dea9b6ef0d02800b413e8a96171a398a1ccf40def0d85b14fe4078f1aa712643f89e4fbff98feb3743fa5e4933ef535ddf202858620740d028c81101109334eb073825b7feca2a782760009a19a83d5e525db79201023722cca8485430584c70601a0f911d0c243a103e481818803238c230129d581c300998a100b1b763a4e363e5cc0f8f072c9f0110d84bc08d1c3d2599102a33c385e5ad26995e5dec07ef3cf2ad217382d699b3135257c4a1f991bb4f0448a108d3a571685408cd5c78520999c022c6032fc4fd7b01517d967c9063365c7f5d82c47f0af7aaef1570dd51cac2e367f194513367f2127b6a5bd31321c7c1d03bd76fcec9271b72ee5a550a8793a7d1e982289a558350fc6ddba9357f2ba56e7999cb44ea4eb5a9d97b6ef6e44300df8abcfb2785d34f4744661f06c19b9db4809580a7da6d2fa14dbd2574627b79b8f92b15c9a46ef62a9bb9b647ff42ef6ed77b2b1c83f7f5ef31bc9a63452d2a590746c332f383eadd0261347ea1060d1f56a63fb6d6d90cc56a51b7db4ceb251503c3c3c3892c9c579ed6353b766f686a313052536f65e1b2b9974e833efe49379d21af46b1f3716d96fffaa5f0ffdf6ecf796afd97e9f6bfcecead6f52a523cf23b4bbf6529a55b5f1df73b09fabd1eb658a9c7daefc4d565f6dbc76f0c9276ec9fe3b77f375becb79fcc9342fb28a14da6ee6e8ebb6912c67e41d0d52dfb2d9252d5a88309ddfa9aed6c8eaaa5ff4d5ce77d8e235191781f6ba2525e087a2a5fad62249e4a155bc00255aa95375935af67f686dd7f6d39a35c800e9a2fe9fd5ea18b063283440d08f9c175db8cf19e547323fee17dce6ef2f051cb5588638810358dc14020181c575270a82491e3bc976640c6f6fd5fe53fb57e21e222fd32c185828f96898d59a1816f71038d915d66e81f294508911c66a483511427310f2b6ae5fad9a1240600951ba90005c4265f97322487001c2a44d440b92f2cb4f8999022f16343830c144366e4e0c8c836e1071e3e3c703c405cd810f1a32609139482d2c838e286455b80f0d2920eab0dee12ec37ffac2dba0aa7256553c5018504ac8490113c044c6c68c2f578a1a3c80995ce8c141d9010610992216350726443b372502ed092383ba5caf5ef952538583d73f0553d89795851324f8d2d16324f9113dbd25e950b04bede815e3b7e3eb9b4d90653ea644aa18ccc4782992f33f4621d4b9b6d2f9da8db4c9b6772c2b6966d336d5eca7ef301b142e0affecaad7ba2274f5f14029e0e466de642138a44dfa9b43ec5b6f41cae66e60352f2cbe5887b6f89babb43f6b9f756df7e271b83fc33e8319f857a442e342d1249c73df386e3530b3d1a69a1cd00065daf3db6dfd60ec96c196c1c48eb2cdbc6666565450b8dbc35af7d3c6ad3cb5e91ab4140d363efb59f68418bbef34e3e9927ac43bff6716390fd76b0fa75d16fcf7e77f0355b103c8d9f4f6dba5e5b14737e67e9b714ddb4e9abe37e2743bfd7c56e55ea5ffb9db8bacb7efbf88d43d28efd73fc76f0668bfdf6937956681f29f468d4ddad693e1dc2d86f189eda64bf5b62bc18d5e169d3d76c5f93ab963e38719df7356dc69be9c0d24c10d38961e73989f4cd749ef7c19442cf2375b314ebfa65afd8fdd7962faab1a45cf8925e7025a61c58318025020d130faecba6ca61be8872c0b3f1a0f0b2280000c931492b8f28ac41bb7c06e367069515b7bdf84ca7b7b9c1c0cddae4ff6dfa3fed93b493ff64670c3f1160096b027eb0dd20450cd8110e6f41e6c49b5eb3d71a2e2a2812a19b571b23b3910012a1816431b3a3c7c74f79614aa011ab151c3e4fe213744321160e505080b413542202c4851370f17428566a89133f62d0001022a29af1726b846ce4c870998d3d3a6e5ed26bec03fc931d9f4f6d6d94557a491c85d14890d1c163054f889f111c15a016300f060e7cb86102464c00494e0f102a36885e94f4ec25c6065eb241cc0c809d569e151cb47ed2b8cc866e5e6d0cce65c41a0fce65489aaf7931cca693b17318f115ff7cd9ba9c5751544585aa558515aa50ac9075fb665dce43f94c9fd7f2fc24698d67e2792dcf4ddc7b37a35987fcd6cbb3865f63e8c7729c90298cd8e54e5cd5147acc8ef6c77c4d7f21b1e46ec091a49de689f45e33757797ee93de6b8dfb9dec3cf23f41bf79ae64633a71b529243ddb505cc51f33f4e9d4953c047984dddadcfe6bbb92e775e091c0d152b35b5a442251573a798de2fbf9d4adf2e290c492c0657331be3413830e3df64e3e79d29aae8eefe7cea3fbeda075eca1e3a6fd0ef3f6bc20f812ff7c750bbbad8d99e4989a8e4d934bb7de7aee77b23ac61e76cd8e4e6bc7345bb7dd6f17bf7325afd84fc56f07f179b3e37ef2a418da450b7d3a757777dd4d9772765cebab5bf7db2425751b215ce8d6dbf3969464eff820cd96e2efba556af5812c8a4af9c2faa55ca5a2adbe548ab680555329d54759b7c76e2464e6cd1183b230084e0e9b51703b11b0e328cc5334fe161ac4b170c848e0945601685e92380c6090660003232c0a0c7c2ac6418a1189af05540636760bbd89899b6c6998eefd93bec9f1b9ed185261d20a50c3ca1523a524ad47c1416c3d48b8af154a1c1427aacfa58c09e5032301061b0b9a884031e28603133c72f818000140088643c00f1f4e83f8d263fda80852220493253e06e8687063f3e2c091212d2bcf6647e64ba9910209810e27344c841acd82e7cd4d3e4e498123399fa3fd96a336e5bbc9f4d2dc703446981f2298894101b1c5e5610610191b7e29f2a045890a821d234914e8eca835136450789312329827fcc3147ae171f9e8cd9bca13d5e75226fd99d3793ecfb934ae487f86bce73637a741f19cfc3f68be4f1d2da1ae156643d66aa09a6b8581cfd90de4ec662f27b2a115396ddb3af0c31ad69e0b45d97e1f97bbee76f831d789deb76da32f77d7deedfbab79f782df836f3b6f84432fff3602459dcd6f2fee44f6c31759fb612dd4de6250fc427bb5c7f9f3ee77222dfc469ecd1af8a2056d07865f078a18097b94bc38980a8691b037d38ee9c4a8e9397bf92974e4257dd2754a5b386f85d3b5ba56d60502653997e753d73921082c5d9fdbfc1cca5c563ae64ad67fc4d18f9f639d442f4e1d4bcdb7581f97cf5f7c5e6bdf65cd24ba3196615d5eb3bebc58967581567ebc7cf773a592529b8b994b0a4bcee67c87a196ca3f977f824099ea633e6ffc39943e2a4b7f066b32446972be21cbba4095c43fbd66d6526fed4f5abd4437c6a553474b95c4bf1cf5993dbfe673d3ba742aa8f3541a664325f512dda95b55c652f36eaaa4f45159b975c7f72c2fefd7057aa0c5a3bb7556ccd6fbef0b41eb792137ca9ef7d93b52d99ada5cacb5f36da6f4a551387453736c127ba0842a8804309188e19f5e9a771170fb6d8fda79c8c76aeed0d2380020a393889dc1257c004e1203c058e6147ad94e01a10581b61da97d6f7748ff8ff29f367821e222f9fe21af2ba81331d86bf1ccdc96bf99085cb75e98b6477f30a73625ce23203517ed8c4b1660c68503ad179c1a1a036e8e92000d3f2e562972a1f8b04112c413a30786052b50bc883eb84afcfc18212484f2a1a25b7cb16abc402022d1920a82d11a7a1c2eaf7a82c7cd5bba8e758087b2e3f3a98d5597486f69a3ad9c0a0642ec0092b1e0d3e31148c042840c0f210f0e143a80433a6ec8f800c1d8f4b8e8176b4616deb2c18d09003b6c76da7868fdcc719705a9b968676c4e43c67c6c4e5b329e8c3d43c597fc7325e36edd87129ec493f1e41485f29314b0f6bdb85b77f299be4ed5f959529daea4eb549d9bb6ef7646af0df9adcfce185e5590afb35a93690cc9dd4c5644d3e837ce22b1dc76c25ac24ed323bdc74cdddda5fba4f758e37e2f3b8ffccfd06b7e2bd14c262b6d1a2b4e7b1e57ea447dec9616111111c442b242bb185f988941529d3d9325b493106e7730115af5f6bc334a0e05a2be30455154be51fc403f9d60a80f0461292911044f1f4dd5be9ee6ac72232113deac38748d210a4c141879d002840c761b6d79f0fa12d2887ceef56cc547f0949b0094cecd58f8d58fe8ee71d5ca4299b6d0559b03af7351bac5aa6d64d326925c5a4212913f9379c05d9491187112f011d089e44ab7c84dd0be37bba710e7c4f65184410124a7cfa48b483439820202cc89d2c500981422b510e44638640326a4028f1126427c50c87173a0868d061b1e8644f8d47cc02462786e246000420406019900bb6b10118493fd10b781182da2e6cb3dbe894cc08d98cfcf7ec99116e5bb47d40dc9ac78c1c18458b252046547021b162764cce82120b750b1c214a2010246870d0e1995c6207011f11e0f3c282bfe60e974f098fc53e64524c9e933e982fa1792693ecf19340a81fa97f19abbdc3c24d698fc2f68be49fd048956eaaa9c5ab52cc64a5df6b9ba59ae6eb57a9ca763b50ee8e12e83d613869e0ee74dfbcfb3f8430fc755ebd1360ee4b86fabd6e6ee358b3fcc60c620e73d7eedf32cb875dff75b47ab5ab6165bee3d9c077ecfe5ba7935c4395bcfeb7206c3dae9fef387bdfbb5c39d563b16f43a36bc3ed79fe3355d27d7f5b9be44f58be6262683d9c92f9d230ed2245d977403f32e2059292b55595986cd5c9e495dcf705054ba3677f99983b1a468ca14acffc8a41f3fbffa785e131d919aef903e269f7ff89cd2be499ac773e30bc39abc256d69a9aacaca527e74f9ee660afd94360f3196022a339bf30d725628ff4cbe9965d8e9197cdef83307f52286fa17ac47f00ce57c39556565f4c31f754b6c855eda7fac3a796e8c41a47e82e8873f39d226f6fc96cfcbcaa2a159a73b690511e8a793e72e5d7a322235ef22fa41bd88915b777c4df2f26e59b6b3157f77ebd430d7ee3d0fb4b5eb40eecb5de7d5fb9d2a2d6d1e52daf92e4bdad225137aa9391685364a40103f00883a30e08fbaa12f11cfe1761291c5fe6ef0e0b428003000653a3fcb8040e3314038c24023cba9189e604334244d4e2b414850f2ff36fd67393f91e493b651009b62deeb70ac1fdce525316b77b3292012355500c762b34297ba2d2536172deb354b850842a47d60dc222c806ed8e8f1d3e37a71e4430b1a414121eb018621256418c9d00262000767870e1502429f8f0b12273f7e3a8c8da35931a3a5560087a947c30cac9ed871f396ae63196024d9f1f9d4caea4ba4b7bcd156ce086343440103242445753220098f1e0f0f02418d1c620005270e175262f83019d1b01ad62cbce5029769823d463b237868fdccf1972db1b9685937bf2165266e7e53d27ccd8b5bb39d8c7d43c597fc7335eb721e8a7e0ea24c20a52ca1e904a66c1faccb792c1fe9f3549e9f25d5f1543c4fe5b9797b0f1782f1e4b74e3b6578554bbed22a844c6dc82eb7f222927a8d2826870b6b193bcd92e85d46cb6ef3dca8465a7969512c925f2b74a9d48dbc03a5f14461b5d46c19191414946e547219c5f773a955b48b47514c8297dac5f8c648303dbaebbdfcf2a43a2d3abe9f3b87bbcddbf386e18afc73d52aec5656b3a49956325b3d3ea9d4dddd75b81ee5ec581457adbadf2a2ea0ad3698d0aab7e7a551912810f585296a72f946f103fd748aa13e108cb565fb9ab4da110060e1cd8ac3d7e824201132a2201e07d86fb4e52988d4224585eff249e0b238007004705e0203faf93e09100a770061edcc0ca000c4ef01cbdc5bc5f64efeafb9459b67a1b94568268369dcce6d06852d9a4230b3217945a3b089f369761d213217ad8dca55e34180c68485819f13232d5204c822844e2912a8f0898d843684e839a9a1a3c3070e0b2cc343091d0b055ef4004031034535a0c4c7a9e0038cda0b2743c37ae150ccaa8ea8b9794b875907f8adaa12e92d67340788a71bea1004888908071738602b348050d16cac7280287183c48e9f1e34104c3e317a3563e30a6f29b3638e6067bd4e1c0fad9f40aeb2213217adcdcc6fc8968899df94345ff3e21c572d63a750f125ff4cb9361ab772fa5a4e624b0babb44255a808cabed546e3543ed2c7a1383f4b0ae350380ec5b9397b2e48b4c290dffaeb6ce1540df9faaa1132ad21375a8a8b481a7dc767ba175a50584bd6697add7b8bd4dd3dbadfbdb71af77bd959e47f862e73da28464a7169d2587a8e512c925f7ddaf3b61187208bb0dbd8edbfb6c5f2bc0db82eac969a0d03434242b28d3c6f517c3f7b8d7a5d3c762f272eb18bf1659118f4e83befe5972785b5e8f87eee2c29f2cf54a3b0db56cd9d49318d5bb63add7595d04ef2b4e775776f5b50a946dd6f14165056697a1af5f6bc2fdad95b3fa4d952fcdb66024d5f78a20b58be51fc402f9558a60f04592b2711044b1f3dc9be7ee6ab6e024426bc5971a81abbc2810b283294840fb0cf688e9b085d4af3feebf0d86ff30043c3c0025c043cc3da016612a90704032d0e34b69129e8e8a575826f69a7ffd7f25f907e91e2222dde7781afbae02c12b3ac85edc5ab48c278482b93ca8c8701ccd16a3b1b22b517afcc4b36f4639391619622860f0b1a1b807e048528412464f2a106482d488f16235c50081d205132c463f62830700ac001632768c70891aa061570a1cc1b453a7a4e60467cbcc0ec0a9d47b1f9b31b210bd19fd35d2f695f62c5a6add2dc5c130f335ef8f0b0c1e1440e103f045c20182262c14c0c8b9763c2c6d64850a282e23018195988cd072e3440fed89956f8f07e7af3af2b527bf1cad87ea62455d87ec6ac1aa338d3c874747e0d36e372a72d1977eb4e2830b53a812faec4af289554ed8371b7eef499be8ed57d6ad65987d275acee51dbee7046b00e7adf3f5332b7acc8b6a7a5a06b4cc9dd525644d3f8ae6befb61aa30f436ab9e184d68ca5a847da4f9adcdd4b7893f6939e7dbfe97af43b0dbff6b712cd94b2e2a6d17c4dab592cb7bde09ec795ba133dca7f69d837be2e9a2986d091427b2bea2e2e2424245cc97bb266bcb5e7ac13e791d4c260858673c631138b8fefda6f6e33ad33173fe3ad5d8ff0f60fefe71f3f7bf5fd31fba6380c5be54e5bceca7f49ab499f2bfa1935bd386bdfd7bedf143fe71f9dbcf663feb9eafb32bcfdcbed5a3471e9bb96db3fcc29d69f7d9b69b5f12f2db8e7b9bb731c8e97b4fe2c8a2d67e1ed2828b06625d4386bdf149f9574b1dd61d5b7e6cd712a50f585abda00f58de2077e2a15537d201873b0124130f5d555edf313e7d17d678c9e961b0000c3466d0e5f63510ed0d527dbce84fcb64af33c01648d344edd7eca6feea8506c2c51bf2722d61b2cd10f42496255ed0675180d3b0d2d36f4597f91c00b1117e9a51918461622c029743d9cda5d4187e9666ce4ca2462ba041877b4666220350fadcc6b16e265058899ce88c5c94d468aa1191888522802f448d914fda0e911011afdd0e8696193e3051d813144a3a1e843909029c189eb411748b8882124c0a5c431a38db4c0ea881f372fe939d601adacbe427a491badc1215231c04739b243a0438698be0b46345e83c703508f6bc046011e3b48ee0d9f86bdc8ccc04bbadc9823ecb1f28ce0a0f513c75f16a4e6a195b1f9cc281361f31992e66b5e5c33fbc9d8752abee39fae1977eb441154a9dc3f14550a85a552ad6a1e8cbb7529efe475adce4f92e674255dd7eadcb47db743826dc86fbd3c65d85541be961542a63023773351094fa2dfd86abfe66bfa4b69e5b603567704bde63713ed64a2d22791f44ca3381cbf5668148a337524991d92e76dd095c06aa9d92c2c2121219c09e5328aef6754b7ca8bc5d28a0215dac5f8c64e0c5af49b77f2c993e630aef14f57b7b05abaf5d673bf93a163ec62cb6cf558fbf89d43f28efd74274f4aa17de8ead6fd2e49f96af5c1846ebd3d6f494bf6d60769b6143fc7adbe9507b2e8498a2786dee72a556ce57d5f0c85157e9fcaa3ac9ad7e5c562f7e36b9695130160e1cd8ac197d83c077c4c704000c660b7d11a2f110453c8b8e17ceece6dc312929103051800e26d9f8c21588318511c43cc5cb005c77147dac070358651eb5864182424b8b97d4776a34f69c93a8282ba6244b98632cd56c46ccd4427fa18490e644c63fd8885190529aacf5f4b09b524c10e11371d602ce8d4805281c8c4b831514303c90c1b250c70459ea0f070b32407a68d8b28363c7c0cf11004c9329c8dc928d9b008a1a4c0000296766125665680d139543cb8bd9ff9327028e173b1dbcfa44bd96e6fb42f324e06085120a0c3e0c70f261b1abc8810d3620989232ca71b704544240506002574809030185a50787b16428004f8df94f8e1f1f7a2cc97be14d5e7af657c9753183ecf9964c231be8b67c30d6efe825211f23fa1f93e759184ba56d8e4cce9d160ae15c6fd363fdc363f5364b7edd68c2b9e1cc537346bad9e2d74b9ee82feefbcf583f1f65ce7e1e81571b4defa9ff36f387b4294dbe873a1ada35b9737d06fb55efaf1d4509dd973abad13df9fb9667c6bb6976e1444bb19a29eb71f11fdc97de80471b782b80e1b612f7a76067b806123ecc1b163353d605684fdfb9468e2249dd2754a7708ed86d3b5ba56d6a569198de5f9d4350d078284aec30d7ee6641456356027eb6f9ad18f9f63dd13ed8c347d9aef4e3efe9e7ff75ccabe3f993d71632cc3fab3a06461b12cebd23a6fb27cf7b02347a9cdbb8c820309349bf37d391324fffc7da86999ea397cdef833677c5436be0bd6de155f72b61ccbba3439e23f5a309b20adecef59fd1337c6a453174972c4ff33e9307b6ec1e7a075d5480d44555ac30839ea9fb853563e307d9af7921c8d8fcabead3bb6e16779bf2e8e72138bec0734bb3ce9d77ab949e9dd4499d23aad4835656af34ecace3798d2975689e9a0e6b8d41951b9379a0428bde0c07fb42fbf2340e2a45ca47fc39170c7129625a04583e709ffe0c0d1e139b0e15563d6c51699ba1d8dc91d55dac8a6134a2e25934de4cf24e06ec3bc013184dc849ed33a4576776f4a8cafdd7edd10cd150ce190a12aa8cf630c88746a59c2218387200a8ef4803943142080408190122c261230d1068040e1c1221ab100065704c0021f166e64526074c468f1c4e3e937b6d0902016944879e13f5810d11142dac6892777e94fe3041ea57c6ef63b8a3428df5d3aef0c89cac99426323e88c01334232f0ebc08c0b192c4b32285062e870a102698c4cc7011858624c490f02ebd6c2007f00f4d2686c7e39b241e34aaa03e8f31e7cb8ccaf279ce1f518df3654ab7dce4e633a41325ff129aef51373f242b75d54dad5a16b252977dae6e96ab5bd5344fd42ac8b1e077e2e30fe4e06a33d769deb59c7d8b2dc7fd267658e480a2e7d5d0b3d50b7febb66aede67121078659e38437779a97b3bddbdb6b1fbcd6d3365c6d2762af6a76e3be6edcc67dd98a76c361eeb6101435b1ab9d0dc1ce8a57e77ab3744f57cb7575ae27cf7ed5ec08a128fbf82653c43f5aa56b93be505e989195b25295956518cde579d435cda68183aecb4d7ede600f5235a40bd65f74d28f9f5fbd64de13158d9a6f913e1e9fbff89cd2be479a4be6c61786f578490a0142555556e6f2a2cb772f5d68c7b4b9883d60e0806673bec38d87f2cfe3975986a1bec2e78d3f6fce2761e7cb605d0acd999cefa6aaac8c76f89f97c43cf4d2fea5aa8fe6c6f833eae68776f88f225d62cf2ff99cacac1a9a751f4a53e8403b7d34b7e9d216d1a87907d1cef9246cdcbae35b8e9777cbb29fad9873b7ae8ab97e0f82a1addf17729cfc7d60bd1c54a5a6cd454a3bdfa4495b7ae5432735c72011c94a48034a00d08b0afccf3bf338027418263fc07cc7ad6decf0923800b8be79600b6063ccd6541b9acef1284afcad65995f861523722bd5ae95fe9fc87f6af822c5455acb11a7c8030e0bc91367047b449f34e086db9917387151f30d1d107e90041f9a2b61d3c2c7822024523584ce88001b6c44b8ad64d4c8619b99e168b21f2f407a2ee4ae592e112f80ac784e7647810e3d04720b0f1a38665850c1f0d9c931b203c3c55553e0b8794bbfb10ec7da0ae92d6bb4466603c285093beb73008f0a1d1c62d00ce87c306fd008f1c0a2c88104273a19119276c1c8c0c05b36609900b0bf62278d83d64f99b7ec87e6a195a9f98a1c51d47c55d27ccd8b6b6038197b868a2ff9a70ac6dd3a0f05348525d1e4f474f252ca47f35cdcad2b7923af43757e96f4a633e93a54e766ed3b1bc89593df7aec1cb1aa7ebec6ea4ca63124773b498523d1590e1362b9d9c05abe4e93147a1f47dddda2fba1f7b171bf979d41fe27e834bf8966a393548fc4d2f38ce290fc4aa149244ed421c820ec7676ef4b1702aba566b7b49494947022928f14dfcfa446c52e16432c0a52b38bf17d8d18b4e8acf7f2cb93de0c312af24f55a3b0dbb1e690636a3a36472e8dd58e69b60ebbdf4e7ee7b01d4e68276d9a44eaeee6385b8b72761c86aa46dd6f13958f561f4868d4dbf3c668c8b8d377f2c0158aa2e289a1f7b9c9f43a79dff74a5909bfcfe4d1159a554e004985372b065be2110d3d6c2ed5742060afd11a3520e14bdeeccb6cf249e0d23601420c814421f4389263203c964cc7e52c8630100a112a00a8d28791a29d92436612d2b9727f6e999115c52f9507bc568b43b589409a46d16881827cf9a6c22911a64760060523e7d79bf3925db051e0c6ce0a951947078a8c1878005252333a5a165c231c247c1c098981a107105399a0e4034e061a364728457eec48618085007b4ab9190ae0a29e96a80523c80ccc9e10fa1293ff736bb210fdd9dcf48af65529266994c7c7430e0d353a7068a918d2c91021730171962f748810202a22794151f1b83521501c4693230331e9a0a72cc91f9b99111ebc6ffafceb8e9c5f6f0eed7546d104ed7548aa7189338f4c48e7df60331eb7e9926d35ae9e284cea542bcc2a4c9d56e1eafc605b8d6bf9ba8f63716f92f4875be13816f7256d734124d890def767a6985d7664db99ddd1f466dc6a2c31b50bbfe7dabbadc6e5d3945ab520d09231b3f44afbc5cedddd847769bfe8d9f793ae49bf4df0cfaf99803a9618ef42f23510cd75dcb6827bde66e2543429ff05c2bef1f54a9af8015702eda5a5cbc89898986c26ef459af1d69eb3663887a5968218209c338e750d3cfc9efde4264dfae3f533deda35096f7ff07efef0b353df7fb3af8941d0356ed3e5acfc57b4baf499969fcb6ec659fbbef6fd64fd9c3f74f1da8ff967aaefcbf0f61fb7eb4ae2d1371db73f984dac3ffb264d4ac17f94e09ee7eebe6d416ed2fa73ad2e67e1ed2b2fa8d3da4c70d6be269ed1d2c57683545f9af7b6a950aa0f5cd1d3cb17d60ff5a9544cf5a15031975545a1521f5d9d9fcf700edd77c6e5cc6e272416766933f80a491094187002a1c5089cfc34caf3280200137536d67fc370f0f0b2280030168e0b740502d701d4c3acfeb43a6ad1257de6fb74c8128d88921296ee13493ee9195ec38fd191420804608813da489e2cb2a99a1841a22752a1a00d5aaa3b229b8bf6e5356bd1050d091859d1e220e1850f0c00851938f286cc4a8f043137d6070dcf860817407884014200ad7ebc868c1e0240379f960b4239867ce44c7c10116d030aec8c5860b5848e9bb7f41cfb00ffc88ecfa756565f22bde58dcee0f090a83831e3d9c0f213e28021c4e2a4c6c3b5190905a580e8d1b003430a1d41521ad6f2320b6fd980c614618fd1ce091e5a3f71fc65453617edcbcd61481989e299994ee99a7539cf2405aa5626a08b2bd1696a81caf6c1ba9c67f29d3e8fe5f959d21c8fc4f3589e9bb7f77024d84e7eebb453865d55f4955663327521bb5c494b3c8d4e632d25951c2eac65ec3451a577d9a9bbdb74bff42ebacd73a6daa9a4d5a7b1f45ca35824bfde340ad5993c039975e095c26aa9d92b2b2121219db368178f251504ad7a749af7f2cb93e6b42e2ef24f57b3589a97a263ec636ad34ed6340ad5dddd75b85dcdbadf2429d05621541410e50b57f424f58de207ba4a1543f94030a6602582a0eaa32bdbd7b48bc7eec7d7a4d54e6464383a3062444426468708d86f74c6518516c2dd7bf0b16ceee0b23800e4058101e30721c4602af9fd08f041b849413d037821bbd8a5c954b11d93ff6bf94f357b21e2227961204d8de1e7c6173841ccb5bb6a1284c34773b189222c807ef59383502235172d8c4b166477086cf05e3b0a1cf0887a94483103880408857d178d1ad267084802120386703c847c224c08c5f0f1a1c342c78b9103efd1b458c9e8996000b5a3460710362bafba42c7cd5b3a8e6dc143d9f1f9d4c6aa4ba4b7b451999b20235c14a02faa1b0b664370b0843f6117a045a8e6f683c626848d20168cdba85f2c30b2f0961066cc11ecb0d959c143ebe78dbbac48cd450b63f31832a65846a693b173b664dcad6b00fa09a4a9d478923a857e4ad5be1777eb187ca5af5b757e9614a72be9ba55e7a6edbb9cd1eb437eebb333865b55e4ebac52c8d485e46e262cb134fa8cadf66bbea6b37829b79cb096b0d33479efb1527737e9bef71e6bdcef65e791ff197acd6f245ac984d5a5b1f44ca35824bfd2b4c9c4913a048dd07961b5d46c15159148c4914c1ea3f87e36f56a76f1e8a590b06817e30b2b3de8d167a2b6c83f5bbdc26e63357b2cad45c7d8c78ed9eab0764cb375d9fd76f23b8be525fb29f9ed213e6f76dc5f9e14433b69a14da6ee6e8ecb6952ce8e45b1d6aa840bbd7a7bde19f54b81a92f54d113ca378a1fe828142cf58120ec44258220eaa3aadad7b39c554e00800c6f561cba46130da31c3c1e4ef4bc60b75119ff09c0252883ef7feedc70f0b22800e48b67731a285f7c05295a140a0ae8faf833f6d074fd76fe122952cad2fd94924f7a5ab5033870a96865dbaef692262874371b77103124b5e538f1ca69339504209b8756e625ebe1c10776525868dd08c9f9f0280061e370b24154802646e707e7827768418225c48a9fd38817219119a71e0f3b00640cf041c6c0903306c8518976ec0481f1826305567570dcbca5d3ec0b06925f21bde58dba6eac6c46e4c5460a97c3b5e0872c810f8b131f6c5a68885061b25ce830009ad520d030171919784b961a13843d363b233868fdbcf19705d93cb43237a721471337a72e19d9927539ef3b81a9b0444c3955a9bcc40465fb605dce2bf94a9fb7f2fc2c29cd1bf1bc95e7e6ed3d9c08a621bff5d939e256057d9d550899c2905d8e84159644af97514a0e07d632769aa6d1fb58eaee26dd1f2cf23f41b7798e542b91b0ba24969e6b1487e4570a6d3275248f411661b7b5bc11582d355b4525140a7524938f14dfcfa65ecd2e1647290d58b58bf18d951cb4e835efe59727a575302df2cf914b2b3dd68e69b62e25b826e5ec380c5bbdbadf2327285b7d60d3abb7e79dd191df752a94ea0351e8e9e413c30fe5139410854a7d14c5f6f5ec62b1fbf13567b5131052a221e051a2c7cc1983fd465d8e2268145c8ddf7acc6bed7db11e356d849ade4c30b285453dfa4f13d3b0dd1b39970c134592efff6dfacfd27d22c927bdf37f6423574250800196dead988091110ef7c285696d899f1dd1002943029f9b87d5e535d309d9686388040881030a328903ca82c395232204d3c343e5e9809322460f8c1f064c370b66387c7666565c3540a0b4b8a102e26a806267c58beec5058f1c14ba1418c5b1f1d2925eab102e0fec37ffac32fa0aa72571338686278400808ea5a1a2c28e0f51ae3eb018813b4123e3d4e9009dc40d3b337cd886b95c66a0255564ca00d763b40ce360f54ce3affab979585d70fe32ca4ce0fc859cd896f6c6cc6cf0750df4daf1736bd6e53cd4873a9de6c808f88528550cb5ba7db02ee78d7ca5cf63799ec959f3423c8fe579897baf4604bbc15f9d9665b7453f4f6974029e2d63971331094ba2cb545a9f625bba6b7492ab012919cba569f42e2b757793ec8fde657dfb9d6c2cf2cfa0df3c47b295444cba24928e6df386e3530c6d3275242f01165daf36db6f6b8764b62bde08a475968d820202813a92c965f3dac7a666d1ec1547270c4c6cf65e1b2b3d68d165dec927f3ac75e8d73e6e2cb2df0e56bf2efaedd9ef2d5fb305c1d6f8b9d5aceb5546f1c8ef2cfd96a594667d75dcef64e8f7bad8b24a3dd67e27ae3eb3df3e7ee390b463ff1cbf1dbcd962bffd649e17dac7993699babbbbaea64918fb0dc356b3ec77082a75a32c169af5355bda1c554b1f9cb8cefb5da74aa93e70354fa84f0cbf949794c4545f2a15235985a954c93757b7af69f68add7f6d49a3dd000119bea4177c89ad5a011b257a24e408d77133c67302f886e1c2fb7cede86df210934e109e41dd3e6308f3d141f48703c3ec892b65276cc12560221ddb23fd7f94ff82f48b14176963a0e6383d22839743875623dd49804eca750b1da70ea11d148755a433021cb5176f0b6c06b433fbb1c1e0531291217582c4422999100b8cd000f29343a12342d000b0e3064205951c9b1b173e1d486a9048e8b1f102a28969dd48e1c16204068d4f7978087112b320701ec5e6cf5c951ca43fa7bb5ed2c2c48a4d5b75c970b0a073021422206a91b3426264b0e2834745891719286462f4cc30606343cf8f8ec7525a66213613bc5000f96567faf2e1fd54e66197a3f6e26db1bd4b49eed8dec5ac1aa338bb66383a3f8dcdb8dc696bc6ddba10e5918c88a0505fad215fb852d5bc1877eb423c93d781dda7669de94a5d07768fda7657238a4dd0fbfe9992b96539b63ded025d594aee06c2124de3bfaebddb6a8c7ecaa8e456135a5396a2a7d17ed2e4ee4ec27bb49ff4ecfb4dd7a2df69f8b5bf91682610969b46f335ad66b1dcd6819f4e1ca93ba045f92f0dfbc6d74533c528dd28b4b7a26e62120a8538d2e9c99af1d627074f9cc751c9078b8673c63213021fffb5dfdc665a675cfc8cb7762dc2db3fbc9f7ffcecd5f7b3ec9be2306c953b6d3998ff92568f3e57f4336a3a7170dfd7bedf143fe71f9dbcf665feb9eafb33bcfdcbed5a3471e9bb96db3fcc29d69f7d9b698df12f1bf8e9e4eece71354ed2fab328b61cc4db4ba8af6657601cdc37c5671d5d6c7758f5ad79739cea5379e1aa8aa0bc51f4be4fa5642aeffb64232bf1fb525e5dd53c3f711edd77c6e8693901011e6cd4e610365e70bd80a9d9f9702ac86fabaee7097bfb47339c7e896fed00b128006e455db5006791ab137c6b92c624f53dd433bfbd854438f9654a4a94fcbf4dff59aa4f24f94f0c6f0598d5cea40e27533408335a936d4838840b3952c3a98389e8c8db15031e35a796e5928dd85aa00863a41809c0a2f06180889d1f1c002ae8881830f1f30223e3ea800373c3a17a9021a30133b3a22704c90e88abc7c58fe8826a06eb858016480e19960f1295571d71e3e41dbdc6b2601bd971f9d3c6aa8bce3bda668b8667838e1a2a7454af2cb370637582450e0f88fa9880e860bc4a8088bcd42811e8d70a4b06de91410cf9811d362b213868bda47197f5a839b52c9bbb88b11c9bbb8c335ff2e296ec2663c750f115bf5cc9b85b57327d2754e9738aa23e4d0a4e35efc5ddba9237f2ba54e7e5386bba92ae4b754edabeb301bd34e4b73e2b6378553dbeceea4b9e2d22773341a1a3d0636cb55ff3257d25a4bad9807584952429f41e1b75778bee87de638dfb7dec0cf22f41aff94d441b99a0f4281c3dd326a6e2d7094d2271a2ae400661b7b4db7f6dd3b1bc0dba1058ed243b252524248413913c36f1fd4cead4ece230a43a8042bb185fd86841871ef33efe58ce9aa68eefe7cea0fbeda075eca1e39efddef2b6bc20b812bf5c750abb8dd51c723c49c7e448a5536f3df7fb481d630f3b66abc3daf1ccd665f7dbc5ef4cc72bf64ff1db415cdeecb87f2ca7847651a649a4ee6e8eb36951ce8e295d75ea7e9798be5a7510a1536fcb3b9b217beb8333db899fe34ebe130f444d04262fa4dee7a713ecc4fb3e580214fdbe93375135af671787dd8f2f39ab9cc8c7cc931583ae7047061f9b4b369c07d86db3e52473fcc182c47db16eeff0ee3600cc250ed62f06f3a03d910dca9ba0068ac7182adc7613ef260b3a094494fe8fe4bfd8f822c545eea32da8aca3b02046f3148d4c50520a950de6cb38a4fc654218778659c7081b32076d8c4d8c66686460c1e582e9411b9a11c90c22076635c68b021e828e64e4b42224819c0091420581d2648180a3c38a9d0d3321528c886078089998c904a0d1c20008124d0b8b4e35fa71f392de631d601a8ad506a4979cd1178f068e0ea01a035000259140ba8190e0730ab3193047254684b878d909210a5255681d9798d877c9961a1300765879d2f867fde4711bbb2173d0c6cc5c66148b662e435fb19f8c1d43c577fcd315db6adca7ea169894a8541ea524dec75ac93a9dadc69174a88e6b717e92b48713e1b816e7e6ec395a4987437eebe5296257ddf85ad6994c61c6ad368202a24217934a8df65512769a29d3bb885cf23f3f9779ede4831a416954487af6a1181cbfe6742ab59d38905cc26e7d6dc0992b2b1f1f1fdb29e522c5f773aa5be5c5a14945018acfc5f8c250291d7acd3bf9e4497b1a747c3f772edd6fffac630f1d37ed7798b7e7fd3ed7f8a7ab5bd8ad58b3c931351d9b28966ebdf5dcef24e8187bd8a28fdd6f1fbf3348deb19f8edffee1e44925b48f379d4a75776f1bad4f393b064157b7eeb788ca93d5179c4dadbc55f7b16889aa0bc1cef39313d8aaf33c98090bf4bc938eb2644d40c784372bfe6c42a117120eade843f900fb8cbe5c44843675098bf86fca3601888b74344c711e007820ccf00a081452d8a2057402c8b063491bda742e9949486713f933f92db4e2acb99d5e644be59d22db2db213dba3c4cd0a37c48817a9151b2facc7bae6a4d2a13d264e0ea8c89005e3b2004905255b6660c811c508484502204088a068d082c64e012d3e98201091c4c04cde819243a0888c0130a474c1222324850d1ea60f5448b82051b8c93f58bcb8c3c7d116f8cef89cec56a3f209db1d96d6e6c48c12479e6c58f24281831d3354600e5ca12920a4c38a29374c541c566a04183120c69c373335aabbc30a1e4401fcbf15c9e1af7ef2c49ff40bebb1ae29df941ac7e71897503eca378576dce2e6362a16237f1499ef54274baaab855110a5a1cfcdd5c2bcdfa8e66d54a356bb765435d106feb51cc7fdd679ddf5308741d00bd5d116aa6f69ed425ab77dd5b3f78a465afd0a86be6bbd6aef776be7d91f7da5a3af7fbfedf346986ad7d3b8efb106d60fdbef7b6cf1f5c05b4755fb38ec51bb7db6d28d8abcaf13792046c29e0c2d0ef600c348d88b65c77486dcb418bbfe1589e2a5fcd2f32a6b182d86d4d5ba5a9eebf3c9722cc7a99e73401022f43c6ef131289369e98831597f144efefb1ceb216971264a67bec1f9573f7ff0f99c7deb8943725f2cbb595b714e98e0f178ae4fcca32cdf7d8c99a4d5e66026c32042cee67c7f2009933fd63f7e3e19eb691eeffb1854be2a2bdf7473f89136185b90c7737d26897f69c54cc2b4b37fe8e99adcf796529d2c4d127f8dca63f6dc8acf45cfa5333f228e952f8098a4aec9bdb2f3014a67de4f9354be2ad33bf76b476d79bf2e8ff3e8ad5613511053eebbeef328c77d5bc51cd7515b5974ae3607e7ec7c8bab7c6598105dccfc9ec026305f8f2a004e3168f897d6e683c2e72610867df36dede2b7eccd579ffd3e5440737a3c0b406c542e93d56c54a5b667112740d56ede38f0b03800ecfe8322e9d8667c721ca070c6d93e061af2ddaa1d48ccb4df04352a92feafe53f6df522c545f2edf82ee0c075c72806d3b058e5a285a0545d93da536a8e4cca61865c2212a09a87b6059badb051b0634b2fe11570fbf1f0d193a181d79342e0a3c302c7890fa0cb428668d2f253e0e6a65091caa1c133a346923302c4ca4ac76755730303a7865282237412ab2468dcbca5c7d8073846767c3eb5638585f49636fa827161f3e2752044a3c52a04861d11ae0cc0468d0e979296810b285c3c3e38086a5e3a96d29a81b7446131036097d14e090e5a3f611c6641350f6dcbe62e72d4b1b9aba4f99a17bf663419bb6bc6dd3ad3671211a1a512f8852694cc94aa7d31eed6953eefeb549d9f258de940ba4ed5b969fb6e2614e390df3aed1c31ab82bed26a844c5548ee16b20a3dd1593d4554729b0192e87df4babb47f745ef63e37e2f3b87fc4fd06b7e1bc97821abf6c4d2b30cc521f995429348dca84b9043d8adcc0e3a11582d35dbc4c4c3c3831b917ca4f87e26b58a76b1282a59b092b9185f9947d2a2b3bcd05764917fb25a85dd8e358b1c53d3b1e99db46c6912a9bb9be3667a83984eb50ac1a5556fcf4ba3227beb83345b8a9fe35027d407a6a888e913c3efe4232332d4773ac918a4c2d369e4a3a9dad7b4a4556e404761e28d0d3e36976c1d56b0dbe8cb4d049b3ecd14f07ceddd6dee600830a260421149a0180032036cff3f22148602f487e9074f9b6b2c09251633c0876ec231523436d3781b8ba75126a2e0ed9f134fc3d1f8fab4d62201219a8b76d58a7d20701383872dc80693b3d102488d9531b9d83132a241c8020e0713db2aa84603898f0c1e13285824e880464e3560ace88717a2038c1b0348fc380f84b31c2d2716572dd1e3e62d5dc7c2601fac519b9c9d0a1e6c3e3a14308d109a14b6c7ede68508170e0786193a667e40b00200a928dbf5b28a85b76c619900b0bf64278d87d6cf1c6fd910cd45bbaaf90c090baaf9b689f564ec3b8cbb752726f0843a015d44894e4d0c4eb4cfc5ddba93aff475a9cecf92ea74255d97eadcac7d6713b932e4b72e3b615855435f651542a63124773349114bfd65a472b385b57c9da6377a8795babb49f747efb0d3fc469a954c52ba34969e67148be4d709ed791ca923c922ec7676fbaf6db13caf4b378888887024cf6114dfcf5ea764178f23150429b38bf17d951cf4eab4312af24f55a7b05b58cd23c7d4746c96583ac36cf5573ba6d97aec2a9ed7ddcd71b626e5ec5814559dbadf252690561fdc74eaed7965741c0a88f28528aac0f48de207fae9f442f940f0b5002582e0e9a328dad7b294554e4008ce9b1587adb12f14e059f1632094c15ea3367e3013aca1937c806d49e0ee3800306018c6d4b12c58b0906102782c611046c13202209c40beed751b55145afa3f92ff54934f929ef28b283076a43bc8c18f0934b0563bb33465ca524ef2450df065d4b2832da49a8bf6059be1ac068030d280e920438ee28c0481032d9d069e931fa19220182316a101a12111433c311b09be103b2826f49cf8740054e8796d04d8d0a2c812fdd0608ca0f99cc4ea871c376fe93556051f65c7e7532bab3091ded2465d684a462f56f8cc4808553c4aec1440c900c22822c160e10148d08508382a5e1c321c752ce5350b6f89f26292b083b4d38187d64f1a875952cd45fbb2390b29f360739692e66b5eec32cbc9d829547cc93f5d33eed651d03f42052cd6877bb0c0d5f7d5bc1877eb3cbc94d7b53a3f4b5ad371745dab73d3f6dd8d29a693df3aed946157257da5d5964c5748eed63122a6467fb1d57ecdd7f49453c9ed26ac25789aaad3bb2cd5dd8dba7f7a9735eef7b2b3944daa63a45363e9d9866291fc2ad32a1587ea34b209bbb5b9fdd7b6589e7741770aaba5669b98944a250ea57219c5f7b3aa5bb48bc753c9c688cdc5f88229921efde59bfbca8bfcd3d52dec5656f3c9a9936e1d6cc7345b9f21b49330ad52757773dc4da372762c8aae6edd6f8e90af5619c474ebed7969f4dcea5b79218b7e8478a3e87d2e2202aebcef034158e2f789789455f3ca09c89879b3e21036122188914cb8486136c06ea32e0e14f3671ad8cbb86dedd0280084f794441b6b91eeb9a033436951072cbed0147e02643d04e5a449a44efe3fa6ff2cdd27927cd2c7f5eb0e086b5b00200015e25bb208dacd4edd42e0c4cf9368720aa3532d1c37172d0b8ca6234243c5b46130210443933f98d490c113a4210c6af1510af9949050018c0129dde00cf021817a3a5e72548c00e221d073e2c68562a725000b112e0aaf8fef9012ab39356eded2676c02fc931d9f4fedacc2447a4b1c75c9c008cdb0b948c53049dca44c50988f83ea868d10193c60102b582f7e76645a463aa6c2420b6f79e065766097d5ce061e5a3f651c66396e2e5a169cbb90331c9cbb94345ff36217ad2663b750f125ff6cd1ba9c670074144857ab31b542858e5addbe5897f3443ed3e7b13c3f4b3ae385781ecb7313f71e8d286693df7aed9ce156e5f85aab2f99ae905dae54229a467fd96abfe66bbacae8244713d652769aa7d1fbccd4dd4dba3f7a9fc8ff0cfde63992cd542a69d3587ab6512c925f21f4e9d491bc025984ddda45e08d28281e1e1e1de9e4338aefe753b36a178fa3139012dbc5f8ca4c263dfa338b618bfcb3d52cec7656f3c8a69466335b5dd68e69b64ec6b4930efa74eaeeee3a9a5bcdbadf2123e0ad2a78d0acb7e7add12b70f585241435f2255b7d20284b918820a8fa28c9edeb27004084372b0e61635ff8c0906243285c801d475dae13fb8d3bdecf7fedbc70c344f0769b00a4ce95cec91fea3962d21e70dcbccde0990c2c9c0d48cc2404b389fc9904aaa99f1bdf8109a9b3bbb9b4b2bd25c581b5db23f3c065054021221428a4c7dae523f2b1a34483c0118f09123f8808a8e0e1c90915361a64f08ad494214eb0d2871e09500341879911241863100f1bd075943f20c4602204c71217003e2a3c52ac3c49e918c0e416215edce1c7e807bcc9f89cec5683f207db1d96f6e503921c402220a16628d9d08347cb8d514981068002135eb8c02c11760185118a858309e3c0457477d80083b8c2ff1b9116feea273ffc47a3901e6b97f23da1717c8e7107c4a27c4f68c72d6efe220a31f20791f90e75b223a24a15d5508a5d3054a9ea7e43b36e43335a3777d3300ddbb6ad7ee0e63ee5320b3ea5dba55b66377f35fbdd57415b352ca3db47afc6c136acabdf71b24d56af06765a877d946215536eab1ae7b54ded2ac5b5fec6755ac55c065aede29b811966bf2d03359bd5073f4e9651adfb385a07565ef5646863aa0355e5552f96bd9a91012362ecfa47248877324acfa34c61b4114254892a4dd475593296e350cf321a042d3d8f5b7cacb11494664416ab3f2826ff7d5ef590b43113a433dfe0fcab9f3ff87ccebef5c421b96f65ddacad38172c98a689ba581e64f9ee23cbf4469b839602032d329bf3fd69124cfe58ff785d16e9253cdef7b1a67c9155bee7e6f0235f30b69a69a2aee9f12fad68259876f60fa7aec97d6f27d4c9cef4f86b501eade7567c2e4ed4ccbc348e942168985ed7e41ed97900a433ef9fe9952fb2f4cefdda515bde29aae33a7aabcd340a62ca3d87f37594e3be4dc51cc7a1b692e81c6d0eced9f9164799ca2a1cba98f9fd804454be1549563e2524f02fedcbc3089ffc96d3fa7ac3e76bedb23600700fef61a07620224de7b719283424077aaa01df6788c070302c1ad9e4ff56feaf8d2f525ce4abe8136114364208537075f729e8a4a54a65c7cbb35254d1eb136a30d0690322310f6d0dcb3554e09da5d130d1724345908c20262e230e14ad0ca890d83109b0c303c8099c17271b1c8c918892510c10ac861037a3848e05373c1dca6cc0d16248dbd1886660746a8a1f376fe93d7606ffc88ecfa776acac90de52467378606a4a50574240c04ae8430f17270738a51d27102d0a428a887cf8100631728cb48e4c8d0bbc65849509007beb75de3868fde47196158979686b646e438e28646e735c3f193b878a2ff9a7caa5cd36089f9b3e8a4289292813e82654ccd3d166db038ff3b6d4e667497bb6936d4b6d6eca7ef319e900e5b7fe3a47acaa225f5fb542a634a43643610939d1572e437299f980b56c9d66477a1fb9fbd8b8dfcbce23ff13f498cf4a300e85a53966e8aed34a9b823cc26e61b7ffda0ecbf3c66c24b05a6af6cb0b09098956ea7ca4f87eee3af5ba5824b93460815d8c6f8b6b69d157ed796955e49faa4e24c7d4746c72309dbdeeba6268272d74d775776b9a4f974eddef13952f56255ce8d4dbf3be28c9d734d487f2c0156a52746aa1bcef6ba5ac84df77f2e84accebd7c562f7e36bbeaa366044c39b15832cb13b20f0c022440270027619cdf1150162e2b834a3ed906ef3f0b22800d4e36cbe033907f537fa15e64587ed7ef77ae08f14068a537f10c02e21c9ff27ceff029b81049740c44216d2042e4564e30e622024fc7dd1a968696c35062139176d0c8c46bae0c97192f3e173e3418537ca19006440c3871c382f7e58bc5a98b46c0a5a164c1d5a54372680a0a1204078e8f091c1aa31922010ead1d99100c5091c1cf28112975805ca71f3967eb311708fea288d6d6783ed1623a8840d122b4342347e8010506322c4c9ce0d02333630527a52fcbc6674ec258616def2c1cb0c805d563b673cb47eda1c6643722eda189dcb90b31f9dcb1a5a4ec67e53f125ff74d1bcdd27134f2baa0620a8229ec65104533930e6ed3e15b0047eabcfcf92de3e94ef5b7d6eeafec311c574f25baf9d33ecaa215f6b9542a630a4b74b6189a5d15ffe326aed70c25aca4ed3347a9fb3e77c47c29552585d1a4bcf383cd2579245d82deef65fdb62795e08df28ac969addd2323232e2914c3eaa76f1386a2d60e12ec6575672d0a3bf7a6bd161de9e370c5de49fae5e61b7b39a478ea9e9d82cb99ed9eab2daf4423b59d3265377b7e7e1342967c7a2e8ead5fd4639a17255824dafde9eb74647e7a9502a3064a12b2770144194a75232158842c918b08828540aa42c39b06b178fdd8faf59ab1e894886372b0e61a388898802241556f04cc0aea3343e42e36a59ec7df3825cb8c2d14084320618620c0084184288420484d40112e0125187611042000000084108000004201c2802884f100fbf791f26aaace52e0f66e82886306fe4c9cfe485effd38e6925833acd5ba78810e1ac748f062cd647bf45888635f245bd132a6488b6cea25d60c2cf61071ecbcabbd2775033b9740456304d0c78896390480965cde2d5b4daaeb4e3eb0eea9c4fb82f3c9a9deb226e86b0ed8f0adb3da5fe552a3526d26e5d0dc5f0f5cbaa993e90c2a2cd0a0d6f3fa1c3270ab02ec5cd255fb8b860a6530650e3e6cfaddb222c20f846d8c3b1b6aee8d8a283823771e0babe1075e34e9dd42de1b452e29714a9c7afc7ed2be52c3d2c5dde572c808d4aae4620ff042b9b78ae14e937a17fb65b088e033e097402b59fd6962726016f3e2aac4b074c33d0409d1f6c7f8425b5e7e52d56b0de140f270111c7f6ad5cdf66f49d991aafa23e4e19a65b81bb465acf8107b6287ab18cb7633d18264b8c06b269cb6bf37680bbfe0a14ab073695fe961a0d368d2f124462352d5c3b2a0c6b658bf152c2b621a895e628644274fe2800c1745e58231d61cc2a82bc8d02054e1107284b525d802500079a0bad6cf255fe3b6f7a46e62eba283586797ab5095afd0b831934564c13b755395da276e396e2c869a3162a56a32eb4ae4762d153aec2ce3ca533b85a6a02ac1cea5cd118994e80fa9b96d09996173aaeab27869f3266254a077826295da5abe1357a01d51d157d560e752f2ac2c6d739113286766c9ad64c15b0f8a9447b469aaeab0746971c615f4715af9de1b65a97eba19a8d30abf4b4fb9f463b5fb7bf7235f53c8fa7efecd40b280666ccd00382d2755ee5011ecf92bb89a717db1f5d8a0b61115ecb9fac3d27d4deae942edcbce0d70c80166008c07bf3125b5b792475c98ab233a9d5b197b8c9fafe7fc328bb072e753e654d563edd29cd2b755342e02ea27b46f43c8d45a5db512e16ed1e3bed1f668ce41e3e9f9aef591b49a7320d321b435d886eb3a13a858f4bf88288ad3399cab28ebf349e489dd8d725596c54bbb210e59a00f7dd16181114d092f55fb2a97900f9d0d351a25cdd029ce2898c138d6aa7c4a426389193443187f8b060d501e8f7125c38161b1dfb03017aba50a134cf2a4747173aa0a9d4d09eef2bfdd7bfaa4d733a74222f7a80fb21fd9b950a1cda49af6f40aec4e4f22b18e159d842d593e994c589e012582a5eb7349ff230555f7c8fea95f1c17c14dff1c305ea1ae19fdc9abf9607284963e07966b635bda28458482671129b736787d17ea349b979b462055a04fd1bbb9598e3ddfb0d6c49f2a8eb54b79636a14fa2c6b8c968df9d14dc4aedcd7d665518caccd3111b45da6b30b387dd1d9f4ad5accc722509bbb50bbb2ca4315fb125a87313b212a43ccd7f8fe530033d8e12dfcfc5a2de8ea40ae62ecd77a20a5080699580c4e38e96e0df0ae56d92554547315e25c0a8b2d8b78f0e97fbaab341f2c8d263cadbf12c3d2f5bcd20f2eea21be8c60b10b5361048245675464e89f0a3e711d17569b31f48dbfff918a960eb1e36af0b7fb17bd6f58f7d7e7acae9d4e1944740d0beaaffe3940efaba46b5f4a26b62e3e7473b55dae46d1fb55ae91ba08ab8ad49fb4a2f1510977dfc9e215ff188100bf4b89e6a860c2f3b21541d4a742cb80040a34a48e443b9e7195d4156eaa901624a106ad44e053af6797abe2bcc145e342d86ec345ead845c51f36287a5475a85d7cb55c9bb5a602a52ced5130348ea0fe5a27bc1714e3705292a0295caeb687a06e4b9a46c4183a6ceef58821a81c17e26e05ade822a8cd71df53c0f5b3292315fb2a17e0eed690d1f0c8bacee2033437472878264e08f51bc451f37b4c67c9e21e0f13d132199f3b1f03355f25839deb31ab3f9da8f6889702ca9868068c6c8de674ee17bb825c7b3e405150c60b277097f5e86ca57da506a58befc7c9f255a94de479240d35d72184fa4b13e9152bc9ea174d7b550ce926d225f8a409670a2456ddec665d922562ac21cbab9acd2566d8ba6312b5403d2755a098081ecb00c3fa8d4756d18d2b5cea07f3f7803ea95bc16f475812f71dceb4db7daa7adae72ad8b91c03bd0b173dd7b77441b9cf740310ffca8c4bcd8b98a6aa267b97f2b4ff8b38caee936342071d123a6e2d59ed82005c9500d92a0a090912090000f6b50862901a010020a4916a01aa79976a02ae799b6a03b2799f6a04b679a36a05ba79a76a06be79ab6a07c279af6a08c679b36a09ca79b76a0ace79bb6a0bd279bf6a0cd679c36a4c0d123419010081582096c285581a010020db09c86a2a0ee10ece0fe50fd210e910d611ed11da12f112de13f513e214f914e615fd15ea16817a16ee17857a17f218897a18f6198d7a19fa4c1aee804d927aff6a2a1b987a1b856b1c9c7a1c896b1da07a1d8d6b1ea47a1e916b1fa87a1f956b20ac7a20996b21b07a219d6b22b47a22a16b23b823a56b24bc24a96b25c025ad6b4c26e1b2b11b1a010020c526b26b2a27cb27b828cf28bc29d329c02ad72ac42bdb2bc84c2ca028b1a61a010020e02ccd6b2a2de62dd32eea2ed72fee2fdb30f230df31f631e332eda1027e1a010020fb32e86b2a33817b33ee34857b34f235897b35f64c36626e70301a0100208e4cfb6b2a379437816c389838856c399c39896c3abb4301a17b3a8e6c2a3ba73b943cab3c983daf3d9c2a3eb33ea03fb73fa440bb40a841bf41ac6e42125629020082582058205a57893d7b6eb26c4c43a2b090cc7bb96c2a44d244bf45d645c346da46c74c47f3fb1cdf7bcc6c2a48e548d249e949d64aed4ada2a4bf14bde4cf54ce24df94de64c4ead42e8261a010020fe4eeb6c2a4f847c4ff14c50239892897c50f66c2a518f51fc527c395a947c52816d2a539a53876d2a549e548b4c55c52ba5a21a010020a355906d2a56a956962a57ad579a4c58fcf721b27c9f6d2a59b859a52a5abc5aa94c5b86097dd41a010020c15bae6d2a5cc75cb45df570eacc7c5db96d2a5ed25ebf5fa0b6b1d77c5fc46d2a60dd60ca61b6aebce27c61cf6d2a62e862d52a63ec63d94c6444b1fe321a010020f164de6d2a65f765e4663ff96bdc7c66e96d2a67827d67ef6824f0e0877d68f46d2a698d69fa6a0aedf64c1a010020926aff6d2a6b986b856e6cc8aacf9c1a0100209d6c8a6e6df6def05a7d4c916e2a6eaa6e976fe9e551e81a010020af6f9c6e2a70b570a271227feaa41a010020ba71a76e2a72c072ad73a1c1e5f47d4cb26e74763ecc5e1a010020cc74b96e751ded0e481a010020d375c06e2a76d976c62a77dd77ca4c7853e080e27dcf6e2a79e879d57acace2efa1a010020ed7ada6e7b64738af47d7be16e2a7cfa7ce77d79265c691a010020ff7dec6e2a7e857e7ef27fb9a2467c1a0100208a4cf76e6e805820a7fa45c97e6eff6e2a819881856f826fc8dfcb1a0100209d828a6f83884a647a7e83916f2a84aa849785b00db5af859c6f8626d22fb67e86a36f87aa3577aa1a010020bd87aa6f2a88c388b089336c1d0e1a010020c889b56f2a8ace8abb2a8bd28bbf4c8c77dba31e1a010020d78cc46f8dda8e3fde7e8dcb6f8e4d4516e57e8ed26f8f69f01c741a010020ec8fd96f9072321bf390e06f9196916bfa7e91e76f925b808f201a0100208192ee6f2a93877f93f4940f9f398c7f94f96f2a959295ff96b7d75a7d1a010020979684709771407eab1a0100209e978b709808442aa57f9892702a99ab9998709af5d8afb07f9a9d709bb032acd91a010020b79ba4709c7e6257be7f9cab709db1bc32c59db2702a9ecb9eb89f352a8d871a010020d09fbd70a0f466dd2a7fa0c470a15cfdca151a010020dea1cb70a21345d4e57fa2d2702aa3eba3d8a4b67052ae1a010020f0a4dd706ea55820318efa411a010020f86ee5704ca6fdd0e1ffec704da7288de686a7f32ba88c2af9a93da001914cfeaa537ce898aa85ab75d4ed2b1a0100209fab8cac9d6b974a1a010020a6ac93ada6facde91a010020adad9aaea4dec7ef1a010020b4aea12bafbaafa771b08713f5c81a010020bfb0acb1c5b1b26fb25820d78c4d431a010020cb6eb8b3ae6f84d24cbfb4179591d9b4c6b5bf20dd95b5cdb6e6b6d32bb7eab7d72bb8eeb8dbb9f2b9dfbaf6bae3bbfabbe7bcfebcebbd8281bdefbe86bef3bf8abff7c08ec0fbc192c1ffc296c28372c39ac387c49ec48bc5a2c58fc6a6c693c7aac797c8aec89bc9b2c99fcab6caa3cbbacba7ccbeccabcdc2cdafcec6ceb3cfcacfb7d0ced0bbd1d2d1bfd2d6d2c3d3dad3c7d4ded4cbd5e2d5cfd6e6d6d3d7ead7d7d8eed8dbd9f2d9dfdaf6dae3dbfadbe7dcfedcebdd8282ddefde86def3df8adff7e08ee0fbe192e1ffe296e28373e39ae387e49ee48be5a2e58fe6a6e693e7aae797e8aee89be9b2e99feab6eaa3ebbaeba7ecbeecabedc2edafeec6eeb3efcaefb7f0cef0bbf1d2f1bff2d6f2c3f3daf3c7f4def4cbf5e2f5cff6e6f6d3f7eaf7d7f8eef8dbf9f2f9dffaf6fae3fbfafbe7fcfefcebfd8283fdeffe86fef3ff8afff701008efb01019283ff0296028374039a0387049e048b05a2058f06a6069307aa079708ae089b09b2099f0ab60aa30bba0ba70cbe0cab0dc20daf0ec60eb30fca0fb710ce10bb11d211bf12d612c313da13c714de14cb15e215cf16e616d317ea17d718ee18db19f219df1af61ae31bfa1be71cfe1ceb1d82841def1e861ef31f8a1ff7208e20fb219221ff2296228375239a2387249e248b25a2258f26a6269327aa279728ae289b29b2299f2ab62aa32bba2ba72cbe2cab2dc22daf2ec62eb32fca2fb730ce30bb31d231bf32d632c333da33c734de34cb35e235cf36e636d337ea37d738ee38db39f239df3af63ae33bfa3be73cfe3ceb3d82853def3e863ef33f8a3ff7408e40fb419241ff4296428376439a4387449e448b45a2458f46a6469347aa479748ae489b49b2499f4ab64aa34bba4ba74cbe4cab4dc24daf4ec64eb34fca4fb750ce50bb51d251bf52d652c353da53c754de54cb55e255cf56e656d357ea57d758ee58db59f259df5af65ae35bfa5be75cfe5ceb5d82865def5e865ef35f8a5ff7608e60fb619261ff6296628377639a6387649e648b65a2658f66a6669367aa679768ae689b69b2699f6ab66aa36bba6ba76cbe6cab6dc26daf6ec66eb36fca6fb770ce70bb71d271bf72d672c373da73c774de74cb75e275cf76e676d377ea77d778ee78db79f279df7af67ae37bfa7be77cfe7ceb7d82877def7e867ef37f8a7ff7808e80fb819281ff8296828378839a8387849e848b85a2858f86a6869387aa879788ae889b89b2899f8ab68aa38bba8ba78cbe8cab8dc28daf8ec68eb38fca8fb790ce90bb91d291bf92d692c393da93c794de94cb95e295cf96e696d397ea97d798ee98db99f299df9af69ae39bfa9be79cfe9ceb9d82889def9e869ef39f8a9ff7a08ea0fba192a1ffa296a28379a39aa387a49ea48ba5a2a58fa6a6a693a7aaa797a8aea89ba9b2a99f79320100500287b8a873d17d4b01c909231d06b054680460c63813908142010060100202022c8c40c19401e25604412e8102c5cd03c7061fc3c38fabca1e66873bee60619fe1d1077fb8e18e3efdc1671ffcf18e3bfaf0879f7df0879bfee8c31f6efad11f6ebe74ec5c1f7ef4c1c63bce8f3ebdc1471ffcf18e3bfaf0879f7df0879bfee8c31f6efad11f6efac18f7fb839abe2716dfa830f07eb593efcf1863bf8f0879f7df0879bfee8c31f6efad11f6efac18f7fb8e9077fbce9c78886cff1c9477fbcb8c7f0f0879b7ef4879b7ef0e31f6efac11f6ffac10f3ff9e9077f9cf1273ffce4a31f636a3ec60737fee4e31ce6871bfef0d30f7ef2d11f6ff8c38d3ff9c9475fbcf10f37fee0a71f7df1869ffcec56016e42bef05ec857e2045d7df84e6b8b3faed86378f8c18f3eb8e34d7ff0e9073ffde18e37fce1a71ffce4a33fdef0871b7ff2938f2f2d1fe7871f7eb0f14ef3e34d77f0e1073ffde18e37fce1a71ffce4a33fdef0871b7ff2938fbe78e31f6e4eab785c8ffe70c3c875a60f3ffde0c61c979e58c610c713458a24bb649b05b15863d660876fde000062df92b340661189fd7392041ae581780920e5fc4568fe8317e9b3e6bd234c1b1fb0d297fd5236b67fb940a7f48b1f8ddca17404c9b38ca533cfbcc9d71632cf265381001490515b9a6107ddace60a3a404044695601947c32f2ce0101174b330ea00437c98626201552ba290a1afe3baf944ac919c42d58e04d4a650599178952e8b342d0042c101f30a49cbffcec8f92fe968996274079960380f97df16d43944d080250a5b090a0c49d5039f77b1b880c19ca8ce5eb0513c1157528124533f11813cdb24501f4129d5ff329c1a15d0ddc45544aa82e570859e54fa3bcf25d7bf1583205e8b2273276479882f9891e2cf5f80bea181e672182571a0dca1f4d872685bbc2b151e9d9c33732a8f14bac88f772a4cd73b2e28fe3a52d5f36fe2ebc1131bafc7a2db6257ac64dd5d129218f3c726b561489e8e4759437ff3c9ef8d0aba267a3b22aeed4a70445e4a119e6ca48441d7c91222abc11294a5e5cde1f617bb2484b106cb9db1d71cb3fcc51ad218bd2b6914f70d71c99ebd85cdfe945213a6204ef5eb14973763b3a5b389d5a28f3131c07c28d3c4a7c40b71179cded73ba44864a461f84aeef10df5669819e967eca726477a4f173bb40904ac1f60398b85075f4917f409a2ea177ba61236e253ed7ed830a5b4609dfe75c82b646ca4ada2e9f8f7835434f6b8774b9d91d39c89f70477da5455e49117878dbe5aaef29b4c238a242dc99ebe62455995f90eb457204d5f961f515b1884c958c7e843e774e6f7bd879d30ae1b31cee8ea82879f6de3702e0326790a986eab9c9845c638fd4990fd0d2a0c6e51144e42f47419ed75bff493fcd4b7e6d52b4b4e87c7f26464d1974b8c89a73ccd328f2a0910dfe2288f27f4aefb41ff1b04b4af738ba058e68cb77e642b49f5de693b444938e808239a947497d390a3ac3a9012248a5d18c7cbfd521606fd1bc03955fd78930b349f67dd3aea9dd9d4bcf41761a3763e6155fac92e17584f9fcdcf464d0411220a34dcae75e2f7b94923c0aa2ec0b60d426f30f7bbf085aeea654798d409def8c42eeca2cccfb49f9524733f93da1e9ce7e416512a4bc3a4524ff2bac1cc4fa87296d20379ba39ff3aee9a70aecf2adefd0891c193156ee4ffa4684665cee4e369236da2370bf747a437022bab4dd9c4bd0f25ecacadf08ca13453dea7de222da0553ddc2caee4f4a6c8d60969fe43570280b2bc4d2dd9b848ca1bbc9ff92473322eff7dfeb97921609474964bb7647cb11f18817ccbbfe24f59a642e43a1317413f7e1fc8e47319f2cea8f7a3dbd6b5dc0a0419accfd898bed1166f993bc12d986e329940ebc25248def664e48e344182699ffbd17464c455c9fc816cb8dc93ae96e4b22df11cff9d346b4f2e23e0444247b33396d7c4f510cf6c85cc922f78bd76b01d54db854d895bbd372413c424801281fd582e52f72efbcc0c5ff69f071b0461acaaf081782f1aeafca82f34bbd6b617c63a6def8a7d4e290e6441843322f9a7a3132c3e03c25b69cbc1dbb6fb472907a22cc269285337799669ea06b31b20c5eacf3fdbb3150c6a0c305fccc717d76ec7766ae17b73f200b095256e6877f4a560ce1084330a7949744e08f88d842ce13974e22bdc8eaa7555e7b0d51061feacb16a49c0bf6c7f279fe103fa0d72be1cfc039d284b842e4ea36aa3a0a14aabcb75e199f07de4826e80ad1455f6979a52c38bfd4fb9100e68c1013848e3cc8cf501c5171153da3087a51503cf5abd2e000180e8082343c00060350300d0d80e1001408fb45ad26524e84354f72fccfaf59876291bf105b8ab2b4d417fd4a55b048e2ee66014211fbe279518bff5283eb7f374613e527c9f922ed061da0ecfcdf02e96bf680f3ff884e36e8fc3ca093073dbf07e4e441e7ef013579d0f97d40271b747e838ff0fdf5970d5765f942b21b41f7df6dd58530af7d28467dba5593"
diff --git a/testing/snapshots/rosetta.md b/testing/snapshots/rosetta.md
new file mode 100755
index 0000000..791ec64
--- /dev/null
+++ b/testing/snapshots/rosetta.md
@@ -0,0 +1,1467 @@
+# Rosetta snapshot
+
+This document describes the contents of the index snapshot defined in `testing/snapshots/rosetta.go`.
+
+**Table of Contents**
+
+1. [Blocks](#blocks)
+2. [Events](#events)
+3. [Balances](#balances)
+
+
+## Blocks
+
+### Notable blocks referenced in tests
+
+| Height | ID | Timestamp | State Commitment |
+| ------ | ---------------------------------------------------------------- | ---------------------------- | ---------------------------------------------------------------- |
+| 0 | d47b1bf7f37e192cf83d2bee3f6332b0d9b15c0aa7660d1e5322ea964667b333 | 2021-05-18T11:27:13.2430864Z | d85b7dc2d6be69c5cc10f0d128595352354e57fbd923ac1ad3f734518610ca73 |
+| 1 | 9eac11ab78ebb9650803eea70a48399f772c64892823a051298d445459cdbc46 | 2021-05-18T11:28:43.2430864Z | d85b7dc2d6be69c5cc10f0d128595352354e57fbd923ac1ad3f734518610ca73 |
+| 13 | af528bb047d6cd1400a326bb127d689607a096f5ccd81d8903dfebbac26afb23 | 2021-05-18T11:46:43.2430864Z | f1c1f5c90fe05e12c8bbc9247364eb723329f25f6c5d7c9509f7df04b6437990 |
+| 43 | dab186b45199c0c26060ea09288b2f16032da40fc54c81bb2a8267a5c13906e6 | 2021-05-18T12:31:43.2430864Z | dfdc6f7758f01a67f5ec92da1bf7ba9c7a41daed3fd61c99e645bc268847d374 |
+| 44 | 810c9d25535107ba8729b1f26af2552e63d7b38b1e4cb8c848498faea1354cbd | 2021-05-18T12:33:13.2430864Z | 30cad510db6eb4a00279e78273a7288fba33d1cc2a6c4d3410b9ae20e3a0efc3 |
+| 50 | d99888d47dc326fed91087796865316ac71863616f38fa0f735bf1dfab1dc1df | 2021-05-18T12:42:13.2430864Z | 9828c2748b63703fffc2f212621c471eacfb9206bc9bee2226897e64f6b00240 |
+| 165 | ad5f39a9f8d95ba4bceef55a9ca753bb797dbe847a8e80ea784139ac28d9833c | 2021-05-18T15:34:43.2430864Z | 6482407f923a098e28f30d59e111f805529a4f0c14cee8eff815e5fb5d223d24 |
+| 181 | 0b11ddfc1d324ee830f27648166d1e52c5868096f43f840f7bd39a0be7346a11 | 2021-05-18T15:58:43.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 425 | 594d59b2e61bb18b149ffaac2b27b0efe1854f6795cd3bb96a443c3676d78683 | 2021-05-18T22:04:43.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+
+### Full block listing
+
+
+ Show all
+
+| Height | ID | Timestamp | State Commitment |
+| ------ | ---------------------------------------------------------------- | ---------------------------- | ---------------------------------------------------------------- |
+| 0 | d47b1bf7f37e192cf83d2bee3f6332b0d9b15c0aa7660d1e5322ea964667b333 | 2021-05-18T11:27:13.2430864Z | d85b7dc2d6be69c5cc10f0d128595352354e57fbd923ac1ad3f734518610ca73 |
+| 1 | 9eac11ab78ebb9650803eea70a48399f772c64892823a051298d445459cdbc46 | 2021-05-18T11:28:43.2430864Z | d85b7dc2d6be69c5cc10f0d128595352354e57fbd923ac1ad3f734518610ca73 |
+| 2 | c14c27a0cc21f39d9fc5e661d5bc2b267eb16ffb0adc40cb56945d9ca84eb313 | 2021-05-18T11:30:13.2430864Z | d85b7dc2d6be69c5cc10f0d128595352354e57fbd923ac1ad3f734518610ca73 |
+| 3 | d8d5b941627ae2d3cef38bb388d3e59bc9ef0c50a1f676f8ad1ec2e7cf895d6a | 2021-05-18T11:31:43.2430864Z | d85b7dc2d6be69c5cc10f0d128595352354e57fbd923ac1ad3f734518610ca73 |
+| 4 | 6a435aaaf30faef9a87a7bbd0b687a97d7e2aaa48978c8f54f026d80074f5fcc | 2021-05-18T11:33:13.2430864Z | d85b7dc2d6be69c5cc10f0d128595352354e57fbd923ac1ad3f734518610ca73 |
+| 5 | 7dcba1fc32e5b6dac03d7cfcaa4e9d47f1e6a69e2af5bb06afb60e184d66b584 | 2021-05-18T11:34:43.2430864Z | d85b7dc2d6be69c5cc10f0d128595352354e57fbd923ac1ad3f734518610ca73 |
+| 6 | 885eef0a6986692f3acff3b7e5d57c61b9eccfddcc75a2722b8dc0ca97c4cd90 | 2021-05-18T11:36:13.2430864Z | d85b7dc2d6be69c5cc10f0d128595352354e57fbd923ac1ad3f734518610ca73 |
+| 7 | 462aaf6e5608babe0863a657da1eccade5a7e2fb8f0702fa96bb36c2de5eed64 | 2021-05-18T11:37:43.2430864Z | d85b7dc2d6be69c5cc10f0d128595352354e57fbd923ac1ad3f734518610ca73 |
+| 8 | 3de7a2232681cf1556a0374c48d32249548b7ef347f404d5252a652f23c23a56 | 2021-05-18T11:39:13.2430864Z | d85b7dc2d6be69c5cc10f0d128595352354e57fbd923ac1ad3f734518610ca73 |
+| 9 | b0e9b4314ac2023e1894a06b43433523b8db8af1843e9797cc9baf4721914976 | 2021-05-18T11:40:43.2430864Z | d85b7dc2d6be69c5cc10f0d128595352354e57fbd923ac1ad3f734518610ca73 |
+| 10 | f79deaf51438b60b9971a32644546c688830a649910d3d5beea7bb1b46fe436d | 2021-05-18T11:42:13.2430864Z | d85b7dc2d6be69c5cc10f0d128595352354e57fbd923ac1ad3f734518610ca73 |
+| 11 | 46d12fb8a054f12ecf34eb34e549776b0e5a06ec167d3a161cc42df4917c891c | 2021-05-18T11:43:43.2430864Z | d85b7dc2d6be69c5cc10f0d128595352354e57fbd923ac1ad3f734518610ca73 |
+| 12 | 9035c558379b208eba11130c928537fe50ad93cdee314980fccb695aa31df7fc | 2021-05-18T11:45:13.2430864Z | d85b7dc2d6be69c5cc10f0d128595352354e57fbd923ac1ad3f734518610ca73 |
+| 13 | af528bb047d6cd1400a326bb127d689607a096f5ccd81d8903dfebbac26afb23 | 2021-05-18T11:46:43.2430864Z | f1c1f5c90fe05e12c8bbc9247364eb723329f25f6c5d7c9509f7df04b6437990 |
+| 14 | 26b9d551b9e04a9f1b83686c99d2250e64c33274fca09c8f084cbc07c99e6b25 | 2021-05-18T11:48:13.2430864Z | f1c1f5c90fe05e12c8bbc9247364eb723329f25f6c5d7c9509f7df04b6437990 |
+| 15 | 8b4144109e8dd53bcd9020cdcf8aa07d0857782428e8ce8983caa83036c986ce | 2021-05-18T11:49:43.2430864Z | f1c1f5c90fe05e12c8bbc9247364eb723329f25f6c5d7c9509f7df04b6437990 |
+| 16 | 4e7809a37e2e5d92f535c164054e9f4b62845ba35065622aecc6bc9f22ed8705 | 2021-05-18T11:51:13.2430864Z | f1c1f5c90fe05e12c8bbc9247364eb723329f25f6c5d7c9509f7df04b6437990 |
+| 17 | d3e38b482a54d92f83a455bc51fa5a97d27fd10ca8f0e02aa1f8dec0d7cbebf2 | 2021-05-18T11:52:43.2430864Z | f1c1f5c90fe05e12c8bbc9247364eb723329f25f6c5d7c9509f7df04b6437990 |
+| 18 | 964d906f0faa2e36fecef73dd3d928a7d4d6b44e4692f641729f26dc4e50bbc3 | 2021-05-18T11:54:13.2430864Z | f1c1f5c90fe05e12c8bbc9247364eb723329f25f6c5d7c9509f7df04b6437990 |
+| 19 | cf169dd63363bb4233f1c0072e70cdc5232cc6fc79dce4419bbac6b3af9b721c | 2021-05-18T11:55:43.2430864Z | f1c1f5c90fe05e12c8bbc9247364eb723329f25f6c5d7c9509f7df04b6437990 |
+| 20 | 7b13a2d59f0f5ce8e9596385dbeae195cfbf7b2a6dd23856b39c97aa616c6872 | 2021-05-18T11:57:13.2430864Z | f1c1f5c90fe05e12c8bbc9247364eb723329f25f6c5d7c9509f7df04b6437990 |
+| 21 | 2de272e4ff48ba9f4ef0fc078984db7db717575b69ed3f57734f985a09e188cb | 2021-05-18T11:58:43.2430864Z | f1c1f5c90fe05e12c8bbc9247364eb723329f25f6c5d7c9509f7df04b6437990 |
+| 22 | 525c36fea6662e4feae932c6a84667ac3e9093f2616e1096a1dfbf1a3a0a7fcf | 2021-05-18T12:00:13.2430864Z | f1c1f5c90fe05e12c8bbc9247364eb723329f25f6c5d7c9509f7df04b6437990 |
+| 23 | ba629862f3600411f9f63f532517b35a3a79e0ed80d1276ffce14abc2b827656 | 2021-05-18T12:01:43.2430864Z | f1c1f5c90fe05e12c8bbc9247364eb723329f25f6c5d7c9509f7df04b6437990 |
+| 24 | 69e53d6a715a6703b4cbcc07002567da45a7872fc9b478b7da0063cc2ac9080c | 2021-05-18T12:03:13.2430864Z | f1c1f5c90fe05e12c8bbc9247364eb723329f25f6c5d7c9509f7df04b6437990 |
+| 25 | 2881c83e5f2048e6e161f5ddcae006202a9cd5fd1b303bbb96ed47d222de5ad7 | 2021-05-18T12:04:43.2430864Z | f1c1f5c90fe05e12c8bbc9247364eb723329f25f6c5d7c9509f7df04b6437990 |
+| 26 | 57287453ba50c26168630bd364ee1d6d5a62cb47e2909d8f41f17c4e4aa401d9 | 2021-05-18T12:06:13.2430864Z | 8b78b87d87dae0305b2e98eba18432d765bdc4ad6467bde6a5e73c421c711663 |
+| 27 | b539473d2bc87092e421043ba92df2c959a68de5b350fe74c83f5dfe8b9385f3 | 2021-05-18T12:07:43.2430864Z | 8b78b87d87dae0305b2e98eba18432d765bdc4ad6467bde6a5e73c421c711663 |
+| 28 | 34d91642f3a2beafd086974c33f6c120170d9db78791e5c87eb2e6e40d4b2dc9 | 2021-05-18T12:09:13.2430864Z | 8b78b87d87dae0305b2e98eba18432d765bdc4ad6467bde6a5e73c421c711663 |
+| 29 | 0a60f4a4cd09a1a648cec0bc53163c41baec35d22305f06351816bde67509e37 | 2021-05-18T12:10:43.2430864Z | 8b78b87d87dae0305b2e98eba18432d765bdc4ad6467bde6a5e73c421c711663 |
+| 30 | 10d16d0016e4b73c70ad95cf43558fa6b0ce1b6bf41f50e27b37d5452a4a6b6e | 2021-05-18T12:12:13.2430864Z | 8b78b87d87dae0305b2e98eba18432d765bdc4ad6467bde6a5e73c421c711663 |
+| 31 | 32f0ec7b34522acc337e29e0347fc73d6fd87613ccf8066d5d3820447f49925e | 2021-05-18T12:13:43.2430864Z | 8b78b87d87dae0305b2e98eba18432d765bdc4ad6467bde6a5e73c421c711663 |
+| 32 | a849f0fbfbf7c242a92933ec3c65731fb952f13d65de39553b5a299b99284b1e | 2021-05-18T12:15:13.2430864Z | 8b78b87d87dae0305b2e98eba18432d765bdc4ad6467bde6a5e73c421c711663 |
+| 33 | 35f00f02ec8927f272f2fd4386f4f9096d3e343018ce268c47725151427c3822 | 2021-05-18T12:16:43.2430864Z | 8b78b87d87dae0305b2e98eba18432d765bdc4ad6467bde6a5e73c421c711663 |
+| 34 | 1ae8a707535496a9dc82e017a712efe8ceeaaa9cc38d237d425db3c14ca91e3e | 2021-05-18T12:18:13.2430864Z | 8b78b87d87dae0305b2e98eba18432d765bdc4ad6467bde6a5e73c421c711663 |
+| 35 | 155a48b8afb77b4983585757fc2180cf95e0ced695c8aef72dd8c342ac4a7e7e | 2021-05-18T12:19:43.2430864Z | 8b78b87d87dae0305b2e98eba18432d765bdc4ad6467bde6a5e73c421c711663 |
+| 36 | 3c4e6814a086760ab805a12740a3d0593e57321250aad9c9f65db0a9de6f364b | 2021-05-18T12:21:13.2430864Z | 8b78b87d87dae0305b2e98eba18432d765bdc4ad6467bde6a5e73c421c711663 |
+| 37 | 68c3201f1493e99db3da84a50d0e87293eb1fb635657290f6264d0afb9be756c | 2021-05-18T12:22:43.2430864Z | 8b78b87d87dae0305b2e98eba18432d765bdc4ad6467bde6a5e73c421c711663 |
+| 38 | 44a30ade9d4f316b2831a7a84a240cb44c796dc03cc5c657ecb00ae947cec2e9 | 2021-05-18T12:24:13.2430864Z | dfdc6f7758f01a67f5ec92da1bf7ba9c7a41daed3fd61c99e645bc268847d374 |
+| 39 | 27a03661a42a5ae071d35802e7fa8747c850a2fbf2bff2b554f0eaada7ecfef8 | 2021-05-18T12:25:43.2430864Z | dfdc6f7758f01a67f5ec92da1bf7ba9c7a41daed3fd61c99e645bc268847d374 |
+| 40 | 874a36755a114485563d3915a7df8066edd41c8931841fef86d411164efd14b4 | 2021-05-18T12:27:13.2430864Z | dfdc6f7758f01a67f5ec92da1bf7ba9c7a41daed3fd61c99e645bc268847d374 |
+| 41 | ef15dd34dab300e445c8b7bbf1e4d8319eec432e38932aaf7d31a7cf4992c6a2 | 2021-05-18T12:28:43.2430864Z | dfdc6f7758f01a67f5ec92da1bf7ba9c7a41daed3fd61c99e645bc268847d374 |
+| 42 | 91c00b22dc9b84281d293f6e1ff680133239addd8b0220a244554e1d96aed8e0 | 2021-05-18T12:30:13.2430864Z | dfdc6f7758f01a67f5ec92da1bf7ba9c7a41daed3fd61c99e645bc268847d374 |
+| 43 | dab186b45199c0c26060ea09288b2f16032da40fc54c81bb2a8267a5c13906e6 | 2021-05-18T12:31:43.2430864Z | dfdc6f7758f01a67f5ec92da1bf7ba9c7a41daed3fd61c99e645bc268847d374 |
+| 44 | 810c9d25535107ba8729b1f26af2552e63d7b38b1e4cb8c848498faea1354cbd | 2021-05-18T12:33:13.2430864Z | 30cad510db6eb4a00279e78273a7288fba33d1cc2a6c4d3410b9ae20e3a0efc3 |
+| 45 | 91d066e112700a042d8e666e1a724ca508182c5febaeba42c56e8ad7ed023b90 | 2021-05-18T12:34:43.2430864Z | 30cad510db6eb4a00279e78273a7288fba33d1cc2a6c4d3410b9ae20e3a0efc3 |
+| 46 | e6b15a9949f53951dd68bca18aa15e5e4ee9122f32da8a0a7f976e251abcf4a2 | 2021-05-18T12:36:13.2430864Z | 30cad510db6eb4a00279e78273a7288fba33d1cc2a6c4d3410b9ae20e3a0efc3 |
+| 47 | 421b0b34b44f236ecc58b9e3318ce0b093dda38d6564f693408786feebc3f79f | 2021-05-18T12:37:43.2430864Z | 30cad510db6eb4a00279e78273a7288fba33d1cc2a6c4d3410b9ae20e3a0efc3 |
+| 48 | 9323b01b72f0f316291d2b74f2abfd90c9736d473125a3dc914eb4f646d6b26f | 2021-05-18T12:39:13.2430864Z | 30cad510db6eb4a00279e78273a7288fba33d1cc2a6c4d3410b9ae20e3a0efc3 |
+| 49 | 0433f72203787a452081ef63f4fb8b33acc73d46a58b73ca02aa6528d05787d6 | 2021-05-18T12:40:43.2430864Z | 30cad510db6eb4a00279e78273a7288fba33d1cc2a6c4d3410b9ae20e3a0efc3 |
+| 50 | d99888d47dc326fed91087796865316ac71863616f38fa0f735bf1dfab1dc1df | 2021-05-18T12:42:13.2430864Z | 9828c2748b63703fffc2f212621c471eacfb9206bc9bee2226897e64f6b00240 |
+| 51 | f72dba7516c401ddcb497be4e58362d5f74fcd736b6045033da4d08bed3b1635 | 2021-05-18T12:43:43.2430864Z | 9828c2748b63703fffc2f212621c471eacfb9206bc9bee2226897e64f6b00240 |
+| 52 | 7f14a3f9d0149c000d41e1f5c352120676c8c7781d929f7900c043a6cdac6834 | 2021-05-18T12:45:13.2430864Z | 9828c2748b63703fffc2f212621c471eacfb9206bc9bee2226897e64f6b00240 |
+| 53 | 3bfbdbaa5fdc85bb9b3bdee204566f975432b0e2b7d863d240878c6ba6ac2c8c | 2021-05-18T12:46:43.2430864Z | 9828c2748b63703fffc2f212621c471eacfb9206bc9bee2226897e64f6b00240 |
+| 54 | 19cf406081a474cf351c4d96a6a199f418f3d4e553fdc5e5bdfee05c00fa4e12 | 2021-05-18T12:48:13.2430864Z | 8e245a1a9029030635764e85a65bf643e49403d9cfd99695816eefbf5797c16a |
+| 55 | 3b461b33a47d69f69d71be2c9997273ed5c1e622fce33a186e03f0e1286d483d | 2021-05-18T12:49:43.2430864Z | 8e245a1a9029030635764e85a65bf643e49403d9cfd99695816eefbf5797c16a |
+| 56 | 449dd45aaa0976fdcc505a3773923f4414e7edd23de1b0c4cba14554252ebbca | 2021-05-18T12:51:13.2430864Z | 8e245a1a9029030635764e85a65bf643e49403d9cfd99695816eefbf5797c16a |
+| 57 | 236633d4f74cc9147289934cec3907b1543b414c482d8001660437f244a1d3e1 | 2021-05-18T12:52:43.2430864Z | 8e245a1a9029030635764e85a65bf643e49403d9cfd99695816eefbf5797c16a |
+| 58 | 04f77122a2251cfaaa8ef44ed87090871b78c489dda5bf0d7520776ea0ae9254 | 2021-05-18T12:54:13.2430864Z | 8aefb14068c23b1641ae67d52a535bda21a281e2ebbbb8df9ff7fe86d1f17240 |
+| 59 | 78683e0890d6f82ea72c0d7e124285ce86f6a85a25e0098a4f3c7297d17fdcf2 | 2021-05-18T12:55:43.2430864Z | 8aefb14068c23b1641ae67d52a535bda21a281e2ebbbb8df9ff7fe86d1f17240 |
+| 60 | 20a69f2c69d854a8b8051153d4a55887ab8b49e61d1dbbe7cce0aa4d4b1c242a | 2021-05-18T12:57:13.2430864Z | 8aefb14068c23b1641ae67d52a535bda21a281e2ebbbb8df9ff7fe86d1f17240 |
+| 61 | af93f5cadffa41907deaef23ad83bf409a164e4ea6264aafebb4f0311c0cbc7d | 2021-05-18T12:58:43.2430864Z | 8aefb14068c23b1641ae67d52a535bda21a281e2ebbbb8df9ff7fe86d1f17240 |
+| 62 | 10800d11b3539203134a273e50daf19f836b28e42128fc5e97a2db8c5e29be5a | 2021-05-18T13:00:13.2430864Z | 8aefb14068c23b1641ae67d52a535bda21a281e2ebbbb8df9ff7fe86d1f17240 |
+| 63 | fa452833c39a5b0aed7f7558882abe2f5267be63b916cf838b052359a66451c8 | 2021-05-18T13:01:43.2430864Z | 8aefb14068c23b1641ae67d52a535bda21a281e2ebbbb8df9ff7fe86d1f17240 |
+| 64 | 9bfe4f5e102ce641a8efb441769e8decb2bbb9abc21e2abb29d79c3ebad0426f | 2021-05-18T13:03:13.2430864Z | 8aefb14068c23b1641ae67d52a535bda21a281e2ebbbb8df9ff7fe86d1f17240 |
+| 65 | 8f23071d95d8620c7f95f79dcb70d9ed5e0799e92b7d9450d240c005f5a843b3 | 2021-05-18T13:04:43.2430864Z | 8aefb14068c23b1641ae67d52a535bda21a281e2ebbbb8df9ff7fe86d1f17240 |
+| 66 | bb884e46baa348dde4f6368e5c85ee6392c29ab5230f2f88a35f48bae798145e | 2021-05-18T13:06:13.2430864Z | ec5f4938501328c0e310864e6e2bf31d82c88381ab27ae1575c699eb43b56741 |
+| 67 | 92aab4fd2e792d3577089f5bef118064be2d2445225f489c19e30dec3631152a | 2021-05-18T13:07:43.2430864Z | fb53985710fc24e8acb1c7a1f6600316b0cdd08cc6cfee3241768ebd4dd736e5 |
+| 68 | 025b9609c4ea9df614f2451ab4e8efcbcfad41ab000b39775e578308aa38feba | 2021-05-18T13:09:13.2430864Z | fb53985710fc24e8acb1c7a1f6600316b0cdd08cc6cfee3241768ebd4dd736e5 |
+| 69 | 6cada2025291884bddc980da0a4395bc557df36eac4e04cb725a76857b8f5c7a | 2021-05-18T13:10:43.2430864Z | fb53985710fc24e8acb1c7a1f6600316b0cdd08cc6cfee3241768ebd4dd736e5 |
+| 70 | 709e69d5d78b044abf629cbf6acf6e66e41ffb932d52a99c134437820002e2ca | 2021-05-18T13:12:13.2430864Z | fb53985710fc24e8acb1c7a1f6600316b0cdd08cc6cfee3241768ebd4dd736e5 |
+| 71 | 0faca24e18619e47e09f8ae7c69986039661987d8f33224d55006d2436eaa2fc | 2021-05-18T13:13:43.2430864Z | 1ccea1c002317b95ba00df08d48bf4381133c4b003920fc37aebf9e3ccef1b84 |
+| 72 | e45902913c022256cb5198038dc48f8b56d10588fbfef6f22a85623978608b25 | 2021-05-18T13:15:13.2430864Z | 1ccea1c002317b95ba00df08d48bf4381133c4b003920fc37aebf9e3ccef1b84 |
+| 73 | db87c08ee0326fab3ccc0a29b034f5ae83b0b4f50ccc9c134ba2a181b985f268 | 2021-05-18T13:16:43.2430864Z | 1ccea1c002317b95ba00df08d48bf4381133c4b003920fc37aebf9e3ccef1b84 |
+| 74 | 92f8e418c3c8e4a45b90be40888a449dcac7e2dad3213cefaa8e83da461c4c2e | 2021-05-18T13:18:13.2430864Z | 1ccea1c002317b95ba00df08d48bf4381133c4b003920fc37aebf9e3ccef1b84 |
+| 75 | 3f32d79a3947aea2120aaa7e3d483ba45372bc9867cf4d1d941db8001a2953e4 | 2021-05-18T13:19:43.2430864Z | 1ccea1c002317b95ba00df08d48bf4381133c4b003920fc37aebf9e3ccef1b84 |
+| 76 | 352b1a58eb7b43a8447b98c5c0bd709063d757a714faff08819777d87a2227b8 | 2021-05-18T13:21:13.2430864Z | 1ccea1c002317b95ba00df08d48bf4381133c4b003920fc37aebf9e3ccef1b84 |
+| 77 | 34be26995b80b5fb0c19f6945f9d298f9a0e6780b6387a6dec815a235312e571 | 2021-05-18T13:22:43.2430864Z | 1ccea1c002317b95ba00df08d48bf4381133c4b003920fc37aebf9e3ccef1b84 |
+| 78 | 7c624da4f661545ce412e06a1705788bcfb6c0abdf2ab24a31831243d7e80468 | 2021-05-18T13:24:13.2430864Z | fd2cfe2173f61a7f7958e5b378bc3de9e86c8a503172a344cd0cf705dde777cb |
+| 79 | f68ba049e47f5a86aa335183873ddc02a90442178ccf3145635e5995e6ffe709 | 2021-05-18T13:25:43.2430864Z | fd2cfe2173f61a7f7958e5b378bc3de9e86c8a503172a344cd0cf705dde777cb |
+| 80 | 83fc62bd6da9c545ab185782ba7a284f71d8f000de296825ae4af2c77cc05e41 | 2021-05-18T13:27:13.2430864Z | 727e8571c9bb7dde071bfa14a063aa28bc6dca3496e5e6dca368cba203c56ed7 |
+| 81 | fee4717b40cadf436215f0020726663a2ac2d139e953a74c7253baa1ccfa378b | 2021-05-18T13:28:43.2430864Z | 727e8571c9bb7dde071bfa14a063aa28bc6dca3496e5e6dca368cba203c56ed7 |
+| 82 | 3a34259eab11e7e2e8b1cf24c7be801db5c01a282b56c114d46cba8646b9f733 | 2021-05-18T13:30:13.2430864Z | 1254bbd63f24a83ffb668bf9c604331c64646429cd5e63b998e11edb9aab9a5b |
+| 83 | afd99056fa97a55c9c3d72f10e1cffb92a9cead44f03bea2c49e0760c5f677d6 | 2021-05-18T13:31:43.2430864Z | 1254bbd63f24a83ffb668bf9c604331c64646429cd5e63b998e11edb9aab9a5b |
+| 84 | f4d1ae2fde7e588f4b0b0b5661859ec7efd4d19c1dcafa0ea9a89d6d705777cf | 2021-05-18T13:33:13.2430864Z | 1254bbd63f24a83ffb668bf9c604331c64646429cd5e63b998e11edb9aab9a5b |
+| 85 | 7fe16f21f7f81add1b6ff98d8e7cdc654d023b6f20485e8b187b156dfa17b6df | 2021-05-18T13:34:43.2430864Z | 83ee06c35b79440bd2608323b17c3b91f071a00799c1bac6f4fd34f0f8d83b33 |
+| 86 | 0d02559b2226f22bbc96c02710d2ea36f9046db9a27e839dd2699d3164103387 | 2021-05-18T13:36:13.2430864Z | 83ee06c35b79440bd2608323b17c3b91f071a00799c1bac6f4fd34f0f8d83b33 |
+| 87 | 5f881c9ce7cc4bb55e13493e5504fbfd02734ba0c660993cd0fa271650d78dd3 | 2021-05-18T13:37:43.2430864Z | 83ee06c35b79440bd2608323b17c3b91f071a00799c1bac6f4fd34f0f8d83b33 |
+| 88 | 93f6c6062fff880bc55f4a9b1b656026b1c5025e16b70db75a54682707152749 | 2021-05-18T13:39:13.2430864Z | 064d2fab7700f63683cf5c88f694c945c83750c22dae78d37079ab14e8bbe601 |
+| 89 | 6ffbd5ac7f663209f30a32d371a57992c08a39708d74f551b0da6fab021f0fba | 2021-05-18T13:40:43.2430864Z | 064d2fab7700f63683cf5c88f694c945c83750c22dae78d37079ab14e8bbe601 |
+| 90 | e3d1016edbb2aa64dd33e0bb42edb9dea272419cc332e5813eada3ddad372f6e | 2021-05-18T13:42:13.2430864Z | 064d2fab7700f63683cf5c88f694c945c83750c22dae78d37079ab14e8bbe601 |
+| 91 | 75cbf0019f0b4879c96e57da042ef2cac2d86bab3b98c51651a38c323705e5db | 2021-05-18T13:43:43.2430864Z | c8a5afcc83323a168883bed7c21386998a199eb8ca9dff9ad3ebbb009bef69f1 |
+| 92 | 208c155c0492f097396094e4804fae1848f11c02e6449d2b2187cd4ddaba5e8e | 2021-05-18T13:45:13.2430864Z | c8a5afcc83323a168883bed7c21386998a199eb8ca9dff9ad3ebbb009bef69f1 |
+| 93 | 2469a9561373b2c5d5b18bd2a04c25dc2b4aedc9a95002dca98457cc248a00fc | 2021-05-18T13:46:43.2430864Z | 24d5532adb1dd4c71f7df1ef4c9d9d36b9bfa3dcafa96a1fc927ded0e5f9d4c7 |
+| 94 | eef5a4311a67ad1df2215838930d3bd222b2fcfea48a281a3cb18bf69839dc18 | 2021-05-18T13:48:13.2430864Z | 24d5532adb1dd4c71f7df1ef4c9d9d36b9bfa3dcafa96a1fc927ded0e5f9d4c7 |
+| 95 | d147a014c55e5916dc00b377684967c45be806ce981a414bce510521809a2dc3 | 2021-05-18T13:49:43.2430864Z | 0e01b1cff044b1a57eaff08863dbb8cd6e33fdd68f33b7182c7161ed027986bd |
+| 96 | fd75e76c6da08829ecae76cd185b56efef108c834e4812b92bd2ece3a309f1c9 | 2021-05-18T13:51:13.2430864Z | 0e01b1cff044b1a57eaff08863dbb8cd6e33fdd68f33b7182c7161ed027986bd |
+| 97 | 04d7c525ff65f0e5fdfe4a628637e3294f2a37b2ea85c252060d1c2fb9336940 | 2021-05-18T13:52:43.2430864Z | 6673405862cd1f38946447d72752682f38ace041903fd5244433fe234f8fb9bf |
+| 98 | 49c737bb5680555d782d7c1ed81763329000fd909f9728b60beb8798e5738fe3 | 2021-05-18T13:54:13.2430864Z | 6673405862cd1f38946447d72752682f38ace041903fd5244433fe234f8fb9bf |
+| 99 | 69ce47995e6f81a40fec705d53f325b235803d54440755d78bc5cb2f40b77540 | 2021-05-18T13:55:43.2430864Z | 6673405862cd1f38946447d72752682f38ace041903fd5244433fe234f8fb9bf |
+| 100 | 64f314baa78d4bb334b950ff265c012cae033cb42d92c2adc6650616371d14fa | 2021-05-18T13:57:13.2430864Z | 8cf8f7344f015297309e91d6fb9b507ef30244b3351d1526b8bc53d94b6a71da |
+| 101 | 753a4b5f0efb3e7fbc3ba813c589ee91412b4d7df67cbd23093df4f6df92ac81 | 2021-05-18T13:58:43.2430864Z | 8cf8f7344f015297309e91d6fb9b507ef30244b3351d1526b8bc53d94b6a71da |
+| 102 | 61d354e63f93ca84689a0b8ffe891dbb87a85e8584f406a73ffc1a6e4b57e80d | 2021-05-18T14:00:13.2430864Z | 5e87bff0f8beb5af8a1bba72dfc64df12e15aac87af27d5ef373cae18cd506a7 |
+| 103 | 1b31f34c0c08ac58dc229c2de76427c1a3cfb641f75e6b73583ea7c9f3fd88c1 | 2021-05-18T14:01:43.2430864Z | 5e87bff0f8beb5af8a1bba72dfc64df12e15aac87af27d5ef373cae18cd506a7 |
+| 104 | a0c534a0071db00b184d3ad6045f9ee1c5e8dcfeebd4deecd81753c80f355151 | 2021-05-18T14:03:13.2430864Z | 16fde7e6f8a1a8cf3234278f6d507c7b5594cf3571dc17e00a098ffd5ca92521 |
+| 105 | bf929e02481c7358d2f9f4546d192d1ab8022f451402186fde3a32aee89a849f | 2021-05-18T14:04:43.2430864Z | 16fde7e6f8a1a8cf3234278f6d507c7b5594cf3571dc17e00a098ffd5ca92521 |
+| 106 | 1f269f0f45cd2e368e82902d96247113b74da86f6205adf1fd8cf2365418d275 | 2021-05-18T14:06:13.2430864Z | a7918512f171bd722d95a505a5e3a7f3217d24fed4af26852e4d3909c5b8a40f |
+| 107 | 22d1747e4cbb168b51eb5f1537e3143f6f37616ee900b1ad6676f651ba70ba9e | 2021-05-18T14:07:43.2430864Z | a7918512f171bd722d95a505a5e3a7f3217d24fed4af26852e4d3909c5b8a40f |
+| 108 | d752a3b69c2dce554e0fde872a9faf54089ef9d8d2da32aaddacc9d176718587 | 2021-05-18T14:09:13.2430864Z | 6eb72623a4b283d0baeaed61abe0f44fa871d3f6806642841a25d87996787ad1 |
+| 109 | 47f4604d3ae1acf79aed4277020812cc9465948e9ec93b3c0dcafbe556214f05 | 2021-05-18T14:10:43.2430864Z | eb6b026c578b0a5b46bfa315475c494c7ead0984e3768d18c100da31c5e9f6cd |
+| 110 | 95c7330cea7d27bf4c0358f22f76828ff560904a1603d98303afec199a97c017 | 2021-05-18T14:12:13.2430864Z | eb6b026c578b0a5b46bfa315475c494c7ead0984e3768d18c100da31c5e9f6cd |
+| 111 | 13b0873ad6008b7cc09fcd4c13d10d81e36675d6a79dae642ca614b2908758fd | 2021-05-18T14:13:43.2430864Z | 18bd63717f1319f3407bae37de01199663017dcb5d935024a457d764bc504ec7 |
+| 112 | 0d6ded8b24b46e1241144babab1aec8fde150b85cb3bb598ffd3e55b10ce3d3b | 2021-05-18T14:15:13.2430864Z | 18bd63717f1319f3407bae37de01199663017dcb5d935024a457d764bc504ec7 |
+| 113 | 7ebf21ec64b9be38256f92611f721a18029c7a19c829236be843e5ccf63e17d2 | 2021-05-18T14:16:43.2430864Z | eda1b1c6e4d4304475962ab28ce49897c35aa424a90cdef39fa83f03a3c61802 |
+| 114 | bd29839c8289cc9b2106b89b7d140e8658254e8522d13cfdda5212f4f3485d34 | 2021-05-18T14:18:13.2430864Z | eda1b1c6e4d4304475962ab28ce49897c35aa424a90cdef39fa83f03a3c61802 |
+| 115 | aee77e983b2034a7b48574c7d8039d36e5bc0720609fe365612f0f6f4481aafa | 2021-05-18T14:19:43.2430864Z | 09641058aa43f2569587de286b6999edf39d64198241ed21afb0a93aaaa4de2a |
+| 116 | 30c8ddabe351a2d05076bbb5bf6417e8c9c8b1bcab45e52df43d026bad8bc2bb | 2021-05-18T14:21:13.2430864Z | b797fdee5191ff693058c4bc726727a3a11b1d7d4137511994228b3819e4032b |
+| 117 | 9ae2bf97fb16f2bfdc71bee44eb6c3a926c65166b4468bd5271209ee409a8e9b | 2021-05-18T14:22:43.2430864Z | eb9ad87094e06f8cc3d30222432412c1452e2211b79cb43f0a06ab260da19bb2 |
+| 118 | df822e30ebddfcf3a36fa711e697c68b58835a116fdde69ae460147e3ddff425 | 2021-05-18T14:24:13.2430864Z | eb9ad87094e06f8cc3d30222432412c1452e2211b79cb43f0a06ab260da19bb2 |
+| 119 | e2448f6453e734ca027acfb8c3b22c884455046785519139af3b2c69c7171335 | 2021-05-18T14:25:43.2430864Z | eb9ad87094e06f8cc3d30222432412c1452e2211b79cb43f0a06ab260da19bb2 |
+| 120 | 15a990fb9f638812822b43570bc381a59b58366d378765e6de94e4ecc2c8e45a | 2021-05-18T14:27:13.2430864Z | 9dfed414b46b5ef758f7c58b9b7e2b4a94cf57444118b2dba4341783eece06ec |
+| 121 | 62d3cebe559aca7d847fddbb8d49123067074f6b46e7ee4ee8ab5e2cd1379bb8 | 2021-05-18T14:28:43.2430864Z | 9dfed414b46b5ef758f7c58b9b7e2b4a94cf57444118b2dba4341783eece06ec |
+| 122 | ed97709859b7f3c20b893e8ba55d83450b5c8c26d1fccec31014f282443bdf6b | 2021-05-18T14:30:13.2430864Z | 0d565a2d4aa3635a1dc3852991a4fe343899bdc27d3cfdbca4533b4f9b8ce3c1 |
+| 123 | b6cc6962673eb7a8a1f04e7de75f6943fd41c1158002133c0641853209fb1e45 | 2021-05-18T14:31:43.2430864Z | 4b63789cd00793df2866017c45d96d99e71a9fcbd5e4236f6f539009c9e45c37 |
+| 124 | 8912d7df71b728a97959c07c89dcf4c53697367149ad7afe1c777fb4d10755d2 | 2021-05-18T14:33:13.2430864Z | 4b63789cd00793df2866017c45d96d99e71a9fcbd5e4236f6f539009c9e45c37 |
+| 125 | e9b02a0cf5ae5efd4db210f05b47ef22b4cf28bbfab3a062741b3201cc4d5c7b | 2021-05-18T14:34:43.2430864Z | 8aa72544f98753fb5e37cad6d230a9d7f2b31017f74b3f96448093d5b1fd4de4 |
+| 126 | e438ce6618873ac6d853b5efb28d8832b5bfafaf3706f836188d1677fbfe1b63 | 2021-05-18T14:36:13.2430864Z | 8aa72544f98753fb5e37cad6d230a9d7f2b31017f74b3f96448093d5b1fd4de4 |
+| 127 | 9782cfc780f1f2972e6801b8b1743a0c7bfaaabeb5a79d3500dc81f4aeb233e3 | 2021-05-18T14:37:43.2430864Z | 353b766bf50975dc39146141117332da69d7fb27b51056717438bf9a34f34307 |
+| 128 | 2c35191bc76956977c2f3d474b258ff716ad10c08ad3987fcddabaafbe841a6e | 2021-05-18T14:39:13.2430864Z | 4dc3f043d7cf64810a34d4b2f3658201ef47a7f5250996b2db01085d8c4ce2ac |
+| 129 | 899def0e9fc956f76fda9f8a498ab6fac88a6942c4ba239b7571e0502d524b42 | 2021-05-18T14:40:43.2430864Z | 4dc3f043d7cf64810a34d4b2f3658201ef47a7f5250996b2db01085d8c4ce2ac |
+| 130 | d157384a46ee60e7a5dea6a1e458db7d53a18bba76dfcd8d0424f9ff92902854 | 2021-05-18T14:42:13.2430864Z | 0cd59ae5d634b28aa9afcd2557618444f75edffe1413e0b6b8b36a104c56cf7f |
+| 131 | 0a4891afe90bad3e12388612df7dda21722db1522dd638929c3ffb3b357fc14b | 2021-05-18T14:43:43.2430864Z | 2aae551a9b51cf3f80540807a8ee327d509241937052d8ea2c731c257962b596 |
+| 132 | 3b87d29df75194f92c09b36792a54002b6bf9c90e7f0ab71fb052211248ac263 | 2021-05-18T14:45:13.2430864Z | 2aae551a9b51cf3f80540807a8ee327d509241937052d8ea2c731c257962b596 |
+| 133 | d350784b49d09d71e906899f47ce0962201e9938e9b9e2cf7489f95964d6b502 | 2021-05-18T14:46:43.2430864Z | 8c7328d7100877bcae64e02381afc2a08c854e2f47aed8820f2a3973dcd51802 |
+| 134 | 5d3490982b5f46e02bb2bfc447f4795b6563234a54d9b7e885c70efe8504017f | 2021-05-18T14:48:13.2430864Z | d5d0d170ec03118f4ccf8695d981986ced44c8161c898339951d2b9dabbd1587 |
+| 135 | 886881a89ccb6d06095fa4098ec5954ebe783cfea6154beb054ee6c37f9c1c0a | 2021-05-18T14:49:43.2430864Z | d719bf010917cb4afe15e75f0f33d19318395ac519e803a0cd0b70d1a653bd17 |
+| 136 | b119938193c278444a12ef37453af54e1fe2353d304816ea0899b7c2e88f82af | 2021-05-18T14:51:13.2430864Z | d719bf010917cb4afe15e75f0f33d19318395ac519e803a0cd0b70d1a653bd17 |
+| 137 | a27a2b007f9131d02b4bbfe655542e3d2946a662440d6ea8ec9778afcac17c99 | 2021-05-18T14:52:43.2430864Z | 1ddaa2d2c730b72eecb4e7ccd18db54689ee8c2e7cadc86bd388eead42421fa1 |
+| 138 | 549c9400deda30646c92c1cbf464f03b4d2e3e392b13235ab02112e27ae82792 | 2021-05-18T14:54:13.2430864Z | 1ddaa2d2c730b72eecb4e7ccd18db54689ee8c2e7cadc86bd388eead42421fa1 |
+| 139 | 174d2b941e99628f09d884dd79022579f1228748426e2e6d7d72f84db0797043 | 2021-05-18T14:55:43.2430864Z | 1ddaa2d2c730b72eecb4e7ccd18db54689ee8c2e7cadc86bd388eead42421fa1 |
+| 140 | 4fa55a014480c3390570e0683fd856e287593b8c2447e3b3b3232f041abc06ee | 2021-05-18T14:57:13.2430864Z | 04d15e560786f01d68f24ce2cf1b4ba4fe2608788485a1062f88fcf1e0fc02ac |
+| 141 | a40a2fb57039e03bd41d7d0d529d21e701c2b5e27e881b6f651389996258930c | 2021-05-18T14:58:43.2430864Z | b99fa59554d421f263f43289005aaae2fea9a44980ec6b59fe5667e1fa4e3d39 |
+| 142 | db794d28c8003517961a47172f90a5c238ff58545ccdadfffccb84bb1767f1a8 | 2021-05-18T15:00:13.2430864Z | b7405aa1bd87311edb4cc621ecccdc1635cfd27af853cb3661dafc886bc44130 |
+| 143 | 0dd40aa8905c73c83b85c001ecac897425fc86386cce16fa6d4d79159dc143a3 | 2021-05-18T15:01:43.2430864Z | a07bbe64cb248cae306f9b062d85b1607d49113742c34f17ada91d618638ad89 |
+| 144 | 1ec9096aae8eee7e1c0d52cfa0dc8a1d1b8650bcfb357c0cf690a3b5ebe84b4a | 2021-05-18T15:03:13.2430864Z | b98f96ed81882bcefbdf62e017cf267153e8f2504e6a3dccf389009025267566 |
+| 145 | 344a71b5f6c73ab49b728f809ceaf53789304f61a73394a170d533c577e31ed2 | 2021-05-18T15:04:43.2430864Z | ef1506c8f69127f1123be303879289613f4c857096003e411e5b8fd9e9a66dee |
+| 146 | edf355798c49c6a5fd5c4ff917182f091ca8e02f4303e27c6d03077f5f3c485f | 2021-05-18T15:06:13.2430864Z | 9d1ebdabb2efc5353c6bd43240a87e98c70c52187388e168a03863c4c50113b3 |
+| 147 | 148d456ee038143404c1ac13f84bbb70788c336b91c52ef7ae3e7f89ce5fd58a | 2021-05-18T15:07:43.2430864Z | 9d1ebdabb2efc5353c6bd43240a87e98c70c52187388e168a03863c4c50113b3 |
+| 148 | e4d020600cc4a475a1c23ab1017e9a09b2a74a6ab56577ef496c92f0ef3ef4b2 | 2021-05-18T15:09:13.2430864Z | fe9240f8229ebea31811c73e887f0fa3beb17fe768000bc9ad036f02c23aa2e4 |
+| 149 | f52fc4ec70e3929472a835ffa42332395f17bf90750944d671e7db66e7a96248 | 2021-05-18T15:10:43.2430864Z | fe9240f8229ebea31811c73e887f0fa3beb17fe768000bc9ad036f02c23aa2e4 |
+| 150 | 277dbb3cd0ca876a57f17d5dd4450b2263a96344dafd6d2bcb39486116b68cf9 | 2021-05-18T15:12:13.2430864Z | 5de5852d321011ac8e6745225fcd76f84c5201c09eb07bf233d9cbcb5d519ccf |
+| 151 | 0337cd8fbeff2167ed00d5414e98a5c6e1f2c8b2f75ecf467200da539e23ef09 | 2021-05-18T15:13:43.2430864Z | 9784898a85c9bfd9e9232bec799ddf39eae6399b4e67a9a7f86cfd179bf76751 |
+| 152 | 62aea07701614d56ef21b3f23c742b3b4df300a544e12f8d75c7cb845b3f1df0 | 2021-05-18T15:15:13.2430864Z | 5bb3538d1afca1834961eadcf1ab536b375097d69e490aedefec58f22bdd9518 |
+| 153 | dd8f3598b71aa75a536d02b7674726f2fa97c6ef8b8b4f42a486ad9f97e24ea1 | 2021-05-18T15:16:43.2430864Z | 5bb3538d1afca1834961eadcf1ab536b375097d69e490aedefec58f22bdd9518 |
+| 154 | 8d03e1b71a5a7c37350664d2cb2583747f84d9ff0b083a1ae9a63da564cd3d66 | 2021-05-18T15:18:13.2430864Z | 594ecfacd915baa073e4deb009b4588b5b0ef4c3fe9cc1fb572f34cd1427e104 |
+| 155 | b3794133eaf9b90aa7c745b18fef0b6044bf8fc63143e9e2189f862470d6fa15 | 2021-05-18T15:19:43.2430864Z | bfd57caa61864a2f9223a36f938255ad361a5fefdb75bf1baaf1591385804f5a |
+| 156 | 53664a7bcb896db9416d12a2efd3c5090a02af16787e2583149f89b917b2f573 | 2021-05-18T15:21:13.2430864Z | ab5eed994ecfd08e09f5ee1ebbcc5490bb71b04de71542479e517d2b452e55ba |
+| 157 | 9859cb5355ce6549e52ba76aa3a93c8be40a573cdfda63619edb1a804534f43c | 2021-05-18T15:22:43.2430864Z | 7d83a6b2cb39996955e8db22afedfc03e689e92a5aa6f846efafadb87e6af39a |
+| 158 | 1d95af97ce25d26f5a2e268172e1779fcf4fc016f3245edd5baca172ca022f41 | 2021-05-18T15:24:13.2430864Z | 7d83a6b2cb39996955e8db22afedfc03e689e92a5aa6f846efafadb87e6af39a |
+| 159 | 62469348a94f75248ab9ca808036348269757ba3a5779792c7156606432c8074 | 2021-05-18T15:25:43.2430864Z | 57dbb8034d6401929f410b8eea365748a8ac4c73dd4da83ce7aefb049a451fae |
+| 160 | eaba8fa79564c8bd5052e954ae7bf6e0dcac40f783ac60f3c7840f064bcdad38 | 2021-05-18T15:27:13.2430864Z | dc2a5a061f391be4c531da188173c0b7e0738168b64333367110894bc639baf2 |
+| 161 | 60edf45d755ed90ce4f07a9cb7465098eac8a8d128ff609419558ada91768e02 | 2021-05-18T15:28:43.2430864Z | 469285a93eac16aeba585922019de7aa36d08a092e69b969149d3b0c30aede2c |
+| 162 | 1992ebdb77c42f3f50b364a7b9c467d14e22a267d4ad7043c2bea0addc93156b | 2021-05-18T15:30:13.2430864Z | 07a8972b80025af851f7b4ccffbeff2d3378052ae31295c4512e221ba39e850b |
+| 163 | 454e495d0970f504d791532849960d3e90ed8d760e3d299df36989645c0f187c | 2021-05-18T15:31:43.2430864Z | 07a8972b80025af851f7b4ccffbeff2d3378052ae31295c4512e221ba39e850b |
+| 164 | 99e4790796133481829c0ed643cb2187a8ab3abf23a1385de7a8f8644035c882 | 2021-05-18T15:33:13.2430864Z | 7e3c7c11cb659b06d28c2fa6a2a5852d23c7e43918830f3c5df14762c02a70bc |
+| 165 | ad5f39a9f8d95ba4bceef55a9ca753bb797dbe847a8e80ea784139ac28d9833c | 2021-05-18T15:34:43.2430864Z | 6482407f923a098e28f30d59e111f805529a4f0c14cee8eff815e5fb5d223d24 |
+| 166 | e6ba944b1863071484d4c16d663df15f52a020dd981df6676cfd40d37bb5c10b | 2021-05-18T15:36:13.2430864Z | b9f0057a1972d9962749b2b7783316e92dc640fdcd16dcd87813e57bb9c1be09 |
+| 167 | f16693295a1dba3bcc4f4412a8d38238e25d9ea81ce2df9632605da2ebf2b6f1 | 2021-05-18T15:37:43.2430864Z | 72dddb734b7626510e8780b461f462a21b1cb9157423f84d8f31a80d4a573cc8 |
+| 168 | 08b190b74a1947f918d22c1327c4ed59e7f34a226a75b00efa78f04ed2a95494 | 2021-05-18T15:39:13.2430864Z | 72dddb734b7626510e8780b461f462a21b1cb9157423f84d8f31a80d4a573cc8 |
+| 169 | 1bda5335ac521b72a9f0f75d116eaf8c372fa8cacda0c10b451d8e14b152d736 | 2021-05-18T15:40:43.2430864Z | 91c424c05bd6bf3948e199966c74782afdb9bd1846092d343104357be1a427a2 |
+| 170 | 49b3dd9c8790426384ec3dd1714dd927e76417f710261a9c18be86a13670aa41 | 2021-05-18T15:42:13.2430864Z | 7e4675e0e1972485e29cca81eef12c8bc1a6df730f4e40bd3a34a5f8b5759fe8 |
+| 171 | 212671f25759a36c11845b7946b9486ed7900c1c2e00c463243888f79bddb976 | 2021-05-18T15:43:43.2430864Z | 2418a2b9448077dcb9b21198878522bebabeca2951513b7c6f6a769c36008a5b |
+| 172 | abea1671ff37d197b933d9465ff2d31fb637c6cc7579f8a1902c52f59e482a93 | 2021-05-18T15:45:13.2430864Z | b73216069fa51b412791d4e634ee3abf00bfb09c74d148c4e7322b2c172cce6b |
+| 173 | 6f4c1f12a6c4a8f8ed832e86e77d6f919029baad6b4c3a683e29de5be4cabd0f | 2021-05-18T15:46:43.2430864Z | 8988ced1d174dd0016c5c1f3f8806ff5ef0915fa3abe81d209b2a40f1b8b90dd |
+| 174 | 54d0cbaa7938d39ef29d525044d9d6e1b0b57f53ac848328a65ee5da6aadbe99 | 2021-05-18T15:48:13.2430864Z | 86dfb6bfee2ea0b2464912e78c925fed820e19c363e4ac4104f0bc7e4385f7d8 |
+| 175 | fbc72e779978afdd55ca32a1f014ba82d3195cc574538f9f660630e47588a94b | 2021-05-18T15:49:43.2430864Z | 86dfb6bfee2ea0b2464912e78c925fed820e19c363e4ac4104f0bc7e4385f7d8 |
+| 176 | 8a3a4329b30f1379ac5cc1553105633ed67a10f2a04b818dd87e585b903c1a18 | 2021-05-18T15:51:13.2430864Z | f1dee8a752c54ea90802e9bfd220d878695ea26b574527d264c27a647a82529b |
+| 177 | 0603b2aeea781d4acd61e5359dd108c07f5510ca7d54293eee39cfc87341c6be | 2021-05-18T15:52:43.2430864Z | f1dee8a752c54ea90802e9bfd220d878695ea26b574527d264c27a647a82529b |
+| 178 | cc9b4d7ba6aa2a90984d272547ec461d6bc8c00c919302f9f7c95e57750fcf8d | 2021-05-18T15:54:13.2430864Z | 08092231caeb7c1d6b6be86fdb6f163c99dcec091715584f3f3b82f57f1f5866 |
+| 179 | 6b344216c981f47439cb3d4b4a785d4d667d76d2fbb27a7732b262218cf21572 | 2021-05-18T15:55:43.2430864Z | 3e1b66e8b9a7d490bad66d983ef55361ac5067844f98c39e525c7fa4c5c04c15 |
+| 180 | 9e33a657088fc6f162c236ee1d8fc4782c9fc1b65baf4a400624f766369caad3 | 2021-05-18T15:57:13.2430864Z | d0b592aebc130af3edf71d7f3125f94550bb54ee7171813ef60d017385a6f643 |
+| 181 | 0b11ddfc1d324ee830f27648166d1e52c5868096f43f840f7bd39a0be7346a11 | 2021-05-18T15:58:43.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 182 | a4bdb5e4bfca4cc40bf0f95188629b4171f15bef7d80177474f182e4abbf1ed8 | 2021-05-18T16:00:13.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 183 | f892234f65bd8848cff65a3791159149a0eaafdd2c1425e0a2fade248a4d1ee1 | 2021-05-18T16:01:43.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 184 | 8e61e99339c9576f65a49331efc76c7befb4fc726bfe5862598563748d881eaa | 2021-05-18T16:03:13.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 185 | ab55b37a91e3a2ac5549c48fb86e1260887e0e321d2eb81bbed63707eaea26cf | 2021-05-18T16:04:43.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 186 | 6b5e55dc25f5117a6807ace7e01d58f1e72c9226cef67e6b35354490a95deb6c | 2021-05-18T16:06:13.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 187 | d7e1b422368de85a3ffe6bcb402f4a835f7a395fc33788d7bf6120bd67bdab91 | 2021-05-18T16:07:43.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 188 | 5de0e742e55a882eac4c25db6d96a1c9fc9b37c1732767ef8733ac3176a29e6f | 2021-05-18T16:09:13.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 189 | a75f85c2b44300ebfecf79b967f89dd10e2b54fbe20c01cf7ce5ce9c5a481263 | 2021-05-18T16:10:43.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 190 | 27425c2392f4b4ee62dd25088a1d8057627c0fab7ec2fc1aba8c05cd6a5ae950 | 2021-05-18T16:12:13.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 191 | b8b6bf01fec03661f63d162f1dd33dcee3222d842e9a7f7c280f1478c4b55058 | 2021-05-18T16:13:43.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 192 | d4b5bae508940c84a0f5361e8974fef7bbb7e0285f7c32ae293a33aa5f845bcb | 2021-05-18T16:15:13.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 193 | a7252cc00f0a6d236d28ce37ed93f16c7f7adae7f70986ce462357ba9bd3a643 | 2021-05-18T16:16:43.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 194 | c7c69d4f7a4e780b7239d60b78364bc5e6e09ca4e6cb6185d6e696141f008035 | 2021-05-18T16:18:13.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 195 | f793a04e7faa4d2f2cb5018f6c1398d97c0f6c41d86b54ce177397bd9517568c | 2021-05-18T16:19:43.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 196 | 3624260521720353bc6d00deb08c410d1fb7d65bdaa828d732cb2bd5b51af42d | 2021-05-18T16:21:13.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 197 | 1dfc4216f703c8fcafe7012f36a93ca6d385005965d423ddeded366012f997ee | 2021-05-18T16:22:43.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 198 | b7e50bbbbd4ec3935c9eae4b5b0df6dd07c2d66ba4588b4a2e80220b422cace0 | 2021-05-18T16:24:13.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 199 | 5b29419cd5c8795052757cbe5a85673bb95f8058367d510a5b614eebbcba25d8 | 2021-05-18T16:25:43.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 200 | 974f931cd575b31b85bd172313f9b096a2d556a0434c588d80aea2e255cc1c79 | 2021-05-18T16:27:13.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 201 | 520dcbb546fb15e57b3e89f6b04546f28d5b8560a663597bd8b346e63886d4bb | 2021-05-18T16:28:43.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 202 | c8bd9ce8c70ddcf480896b5938412897316db7f80a705d2053f1cfdfde75fa4f | 2021-05-18T16:30:13.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 203 | ac74d5e3ff437aeb5dee202a0f2ce2c8ba1b3752c4f9e54ce104334c7cdbca4f | 2021-05-18T16:31:43.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 204 | 09dd1637b08cdac6c6e7ece784d691b5732de8f00c5523e6b53735694667020a | 2021-05-18T16:33:13.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 205 | 96d791d7e3831ef924f7e52b432029a225301a783a2288e73e487a3c96b718e5 | 2021-05-18T16:34:43.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 206 | 0fa5dfdd441bedfe607ce146b96b55ff69d43d0d03361d11cd2398f379341448 | 2021-05-18T16:36:13.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 207 | 6110e202442e0a0d68e7f370fbe3d562cdad41c72cf749dd2c50c7330c28c36f | 2021-05-18T16:37:43.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 208 | a77899acb721101e1bc33364d6cb84519b3631e793ea301fb9b72c26012ddd70 | 2021-05-18T16:39:13.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 209 | cd6c3abedc5a724ad40c57ed47a0e895c50e38c19a0aa62f038aa5a11a1a86ae | 2021-05-18T16:40:43.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 210 | 6796743becb7857e553fd1b2a68fc835fea08aac6bf61680b414fb269cf485b5 | 2021-05-18T16:42:13.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 211 | 1d6c0fc46830895324fc0f59ff3bcd2348e3987ccd36155a063a727af8d04b37 | 2021-05-18T16:43:43.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 212 | 8818de4553835c59b0e147fcb544d5108242631483001d10d12f7430dbf357b9 | 2021-05-18T16:45:13.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 213 | f35abc671d7c0de31190a0c2ad12244350f50816d1f1572f9a0b555ee4b6252c | 2021-05-18T16:46:43.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 214 | 2ca3f52ba048db2283f01251df3d99144dfeb6394af2694d8a3cf21ecc997d8a | 2021-05-18T16:48:13.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 215 | 7122515ea8fff0b4787569c37ff3000c4d82acf50cfc78b68b5b49854b624316 | 2021-05-18T16:49:43.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 216 | 75f4b79788c8e687428cd7a8e6d3063c3c6cd898f0c71765a2f1702f6b1660d0 | 2021-05-18T16:51:13.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 217 | 1f5bedb07a8507ac754296b8e929055a35a1ea64e3fcc19a80bc3fd2f13176d1 | 2021-05-18T16:52:43.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 218 | 9e2907af90207b266194233ddd72bc9be68e51d4e7ba87db5d679d42c2ff1cc5 | 2021-05-18T16:54:13.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 219 | 2807af1e53b9d3c64881a6b4b9858dd7c83c8f558dfc504c56cb898a1c403d40 | 2021-05-18T16:55:43.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 220 | 1d646ba42a53dc8051cc425d07e939241cffcdb6fe6f4fa85821590140cf4959 | 2021-05-18T16:57:13.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 221 | 72c14f1ccd736bad7aa7145638d646fc3ddd8a6fa14bea2598c56922e598fe7d | 2021-05-18T16:58:43.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 222 | 3cd7deb06917e4e5bffac761438e477217390fddd0e734d4bee7b58aa5462a53 | 2021-05-18T17:00:13.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 223 | 6b45aa0a37eb1d73f6ac80e02b267b36bca8468ce6d7672946547d92ea305a3f | 2021-05-18T17:01:43.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 224 | ff4f2dcd541cb36dac0c6911afe0c5d0ad47352cb7d64ffc39d16ac6ea7e1465 | 2021-05-18T17:03:13.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 225 | 2da1f253c5ba84469cd695bfb766d5838f8e06af989a826da11d82c2c7f6b818 | 2021-05-18T17:04:43.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 226 | 5ac846cb24e890a198f9da7b835f5ad728ed503b4586fb21eb26a8a5b36a1dbd | 2021-05-18T17:06:13.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 227 | e0d02d66b7ef3d479690b31d5a469fbabcf61bb3567687d9ab9f4c8cb6ac4b1b | 2021-05-18T17:07:43.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 228 | fe54d0b59780c5271f55d7507813996f49b77904706692a9bf24efb327b691b4 | 2021-05-18T17:09:13.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 229 | fc8cb5781510fa19209f3c35bc227cc95d96155285c2f4fc4ff77271c5c5d092 | 2021-05-18T17:10:43.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 230 | 3948214d2f462a2ecf03d80a74e2747f8bfcd1cb6c25331ededa14dbf7711240 | 2021-05-18T17:12:13.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 231 | 718bb52de77713c1ed5587609a0af721a5892b40f36b9cd0dfff3fd08f99fae2 | 2021-05-18T17:13:43.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 232 | 05427a7fe902b0477de7825209fa045cf44c10e82ca4d0c0a272ec8b19363c4b | 2021-05-18T17:15:13.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 233 | 6b8c5f8b588ebf960dcb4fab49828700cd73e1fc601a23c6df8db1569375cf7b | 2021-05-18T17:16:43.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 234 | 1a1c71ce3c24e1dd133dea8dd5f2e250b127e732b60022dacef4c71a80c07e78 | 2021-05-18T17:18:13.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 235 | 2a6ed4a2bba76eb82fd1f07244986d5f141d18a6ec40e4d840f664407f145403 | 2021-05-18T17:19:43.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 236 | a607053bd0a8cfbfb295b241ea77cb60f0d87a962edd98979ab412cb1e35b8b4 | 2021-05-18T17:21:13.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 237 | 91665a84e58ed4c3799eebbf73100366ecadb7ab4494c3d99f4298f52c7598fb | 2021-05-18T17:22:43.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 238 | 316962c6984bff773b0d6d8733a706c75cc81f2ed0391fa9befb6621bc760e56 | 2021-05-18T17:24:13.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 239 | 51b47f12c6625f968255cab87398e9d3f22a389d70ecd2860376d5bc279e9768 | 2021-05-18T17:25:43.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 240 | 4e9d5be3b93f50042f634994028858256efca70fe5283bb6c9fef31730f920ae | 2021-05-18T17:27:13.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 241 | b1f70f207479cad371316c9d8c82ab643c7424bd2976c4fab7d57f360b446b0c | 2021-05-18T17:28:43.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 242 | 5d7f4f96c1bbbfb113cfc6a6b51bcefb0bdab315d0ec474e3344b647228f4329 | 2021-05-18T17:30:13.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 243 | 8dc933445f4f3599522b2d12e869f7354e75237d2dfae78017499bc9ea4dc23f | 2021-05-18T17:31:43.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 244 | 7efee72a3d754192ef884a978116a36db722ad2839ef0b1ecb7c8c819cd2a523 | 2021-05-18T17:33:13.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 245 | 577566336ed2506d7df7cc3c3e4d8167245683146c43609f079fe2bd22ccdca0 | 2021-05-18T17:34:43.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 246 | 057d684f5a5b9ad7574a5dc378789d8575236ad33177b821b9cd13adacc2847a | 2021-05-18T17:36:13.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 247 | fa26c835647aafbe915e8a53d2841d80d1321a8a01f2fb454727470f50dc5786 | 2021-05-18T17:37:43.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 248 | 533e700c41c903cb5b7613c7a65dc83df5bf7a64cb1f36486fa72e1ebb098d52 | 2021-05-18T17:39:13.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 249 | e3de343510032819760a95e952e8ec3d9d2d4fdd8968d36dc395c3cdab46e85c | 2021-05-18T17:40:43.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 250 | dab2d320a3664950dda9aa34f87884bc135d090b04ec2f5ea4878b9961f95588 | 2021-05-18T17:42:13.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 251 | edda6ae6250c1101969752a2b59229652f39bc53e73dadc3d439cfd2d0b767b0 | 2021-05-18T17:43:43.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 252 | 145b0ca14895aca4dfbf508cf0818e20a092f618fdeb76ab9e8ab0d713c35ebd | 2021-05-18T17:45:13.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 253 | d1904a5e13fab13032318edd1c6a885f0a9ca0a40a6237105973879757dbbecf | 2021-05-18T17:46:43.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 254 | f9c8066c4d853c0ff2fe6edd6c2482ed73f8f6168f782cc14f6729a79f9d4dc5 | 2021-05-18T17:48:13.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 255 | df1023c43b531bb9444f523dbea281a269fb78089f597c8b537c9d2d11bae989 | 2021-05-18T17:49:43.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 256 | 5b7e899aa3d1959c6c1658ce1be5fed4a8db289e79bdc9ddbd469aeb49cec44d | 2021-05-18T17:51:13.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 257 | bcf067ef05264c969ffdb98a72363f2525aa08a8f5014236a18fc0e2ab569dd4 | 2021-05-18T17:52:43.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 258 | 55093c18961167646e22bc3ddbaf290ea5ca0aeb76a890b19f283c2287dc902c | 2021-05-18T17:54:13.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 259 | e2a1b1c048f89a4b1c3ff3c37f05733c4fa41599a9d500fe03912041ac001208 | 2021-05-18T17:55:43.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 260 | 60ad42976f5c92519e3cd6760840b84f743775238dcc33260bd0be59ddc02746 | 2021-05-18T17:57:13.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 261 | 3db91e9aedf2ce1f91585a946f5f7e98bcc616dc3f0bcdeb246c873d807c5e96 | 2021-05-18T17:58:43.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 262 | 09e7aa2f192fdbb6eec4bb8123e6acd7d39503121610f926bc8ea573192ee4fe | 2021-05-18T18:00:13.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 263 | b87fa3c9d5e9a4f83101f20b97b3fc65a1fd9c30e4511629415bbbcd8c03bbaf | 2021-05-18T18:01:43.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 264 | 81d51d440a9d5bbe84c2b2b5fd873e0189d3871e7997bf52d864871498cd8c23 | 2021-05-18T18:03:13.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 265 | 8821d91bcd9bad37a9d557c9ec0436c3f6e36af178124bde09f8f02191da055c | 2021-05-18T18:04:43.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 266 | ba63162cd9941e8b4cd8756455e4296d1d292d0f3c439028ebdc62af22021acc | 2021-05-18T18:06:13.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 267 | 1f7b54a27a9ec4f995c296d166bd46f56961f6f5954d09dec6bc356d26c4661d | 2021-05-18T18:07:43.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 268 | 68aa6bb69da07c1205ba661629f570f8487ce8c99f8014661a791ac0afe11400 | 2021-05-18T18:09:13.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 269 | c59decaf49bad7e7fbb3d41f398f49186208d41b17c5f08412f6da1878d94509 | 2021-05-18T18:10:43.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 270 | 8cbb4175b0ff16ac25a25a8db3de058f0342ac678209154674b0a4b1f86e39f2 | 2021-05-18T18:12:13.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 271 | ebb6211ba03b6b9df91ad861da6eace6538e4c485e40f01bf75b69f15ced3795 | 2021-05-18T18:13:43.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 272 | b73c164767a4054bf1a8b050c8458f91b6b62cb158b98af48f796a2a899e9a58 | 2021-05-18T18:15:13.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 273 | dd06603d6554e2435db1320e4226338855e11536c4eb66282bf243ad25ecb386 | 2021-05-18T18:16:43.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 274 | c72ecaa564adba80c729ccba2634e619cabc427a582d0d6c80ee686968c7f38f | 2021-05-18T18:18:13.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 275 | 5b02ed0db18e0ac69d5ed4594af876670c6d45ae9f12a6ce75580f36c303cba8 | 2021-05-18T18:19:43.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 276 | 0fa3b8049c9d60d304f82c4f77836ef3c19e400a3063e8010d904eed356cbd05 | 2021-05-18T18:21:13.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 277 | 40328e58ab92d9e60a9ded43b3819ae95a9b8e76b62da8c23c4fef3ef2a51168 | 2021-05-18T18:22:43.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 278 | b32b79ae98b3573b37f1be930015671b1dd7a21d4d1dfa3e136fb5a19be8ebe0 | 2021-05-18T18:24:13.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 279 | 81cfbeccd154e07e8177c3d70c53b7d2c250cab0a0b417487dfd3b950ca4dc66 | 2021-05-18T18:25:43.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 280 | 0a6dffd4215b6fe426a574e98eab6777f68385d3caa4852225e2ed5fc396778d | 2021-05-18T18:27:13.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 281 | 26bd68ffdbd09f193acb807848f4031d90017b4a00ffaa8a4a78bfbff99b70f9 | 2021-05-18T18:28:43.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 282 | 0355316b29a3c9851b8261746ce08749b815df676ac249e25cfdf7f60223e55b | 2021-05-18T18:30:13.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 283 | c570491ebd448cf87843c66df7cb2985d23d86c19b8d9b0cb776f71ce6d1db47 | 2021-05-18T18:31:43.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 284 | 209473b8d2f96d50eb1bf72d8e4af42b3cf875d05f1868242667b50a1cfcc10c | 2021-05-18T18:33:13.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 285 | 5c2357a8baa65762b09aba12c3de47f6e5df1fd292320e28119b5462b0770bef | 2021-05-18T18:34:43.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 286 | 1c290e299db20a0702f8825b30ef77a9cedc28cd33c44701dd8f12559da80bbe | 2021-05-18T18:36:13.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 287 | 6b5651db137039b4863fa8281b4b6f3538c1f0bf1e905a5c0e05e8662b34f205 | 2021-05-18T18:37:43.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 288 | 12e9de2466a59a2d740b89f699b9b71c491c12f92dc5ec595f88801343365ad2 | 2021-05-18T18:39:13.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 289 | 75b83e5ba7546eba49295cbfeb44a9291a2993063e242c6d922094174a6e4e71 | 2021-05-18T18:40:43.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 290 | 7c59c54bbc9aad8b01bcefd852e8f1b8389a78cabc637473f660dac1d5963e91 | 2021-05-18T18:42:13.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 291 | eb04e445e211d89eca0f89959dfc6a886796a94947dac6907ce50502b28dd07e | 2021-05-18T18:43:43.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 292 | c84075a9423abc62338f59582aa7a9030a7ada79caf3e025dd4fcbab54c7d1fa | 2021-05-18T18:45:13.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 293 | 87df3d788375a12e8a1c084668e2c57e34577623342d476920e060b90ad88a7b | 2021-05-18T18:46:43.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 294 | ebd1d6479aa7e1dcea2330590b58c339cf443e66db223654807073ba1fdc815b | 2021-05-18T18:48:13.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 295 | a0249b67e3d2a5305ebbddb5bb1f66a88ad644f764e35b33715c4c7298ed5cfa | 2021-05-18T18:49:43.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 296 | 57758c4bbbab72b88d01ba4d6e2474b775c8b47a70a29effcd5e54714d04a822 | 2021-05-18T18:51:13.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 297 | e1180e06a18fcbe96efb6ad8ce8187b6ccda62c87fb81aac2607637ac442ce7e | 2021-05-18T18:52:43.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 298 | 800451121e36018ef3d974a87f0dba7bdac6b1975f6cb24dc82e1799cbf5a460 | 2021-05-18T18:54:13.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 299 | 9c8a1891b37896e34e0906e1d43b15119e1de697d4d1452d5cb700be6411430d | 2021-05-18T18:55:43.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 300 | c3ee9e413e4467c1519464e160253f25ddb7f065c07b54fd59ca2db0eb449955 | 2021-05-18T18:57:13.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 301 | f8d24cdd0ca59b336ae22a78959360cd52ffcfa9c565de4ad2221f749fea0c06 | 2021-05-18T18:58:43.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 302 | 64be1799f09989fe21e0e27cc6001a3107139d3c8ec79a86fd4e79d81544fe38 | 2021-05-18T19:00:13.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 303 | 775c7c964978e692eea2b857a2e2ed6f3e7b48a2330b5329d5dd9922b67b382d | 2021-05-18T19:01:43.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 304 | 6fb38f7dfa58a29643eb1aa3c50d578ccbea9e1e0fda57190d1f482994d92307 | 2021-05-18T19:03:13.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 305 | 53503e25ef7bd351c5dd392953e46161ff5d0d0f62af10e912366b0acc0ce2b0 | 2021-05-18T19:04:43.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 306 | b1133811cd19286c7a578fb5a2ebc173fbc9aa72586f2707c4ff668c703a7519 | 2021-05-18T19:06:13.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 307 | 290c39b15375ed3ca42bcf9190a81c2c48dc75e82bb5b1a4322f1531f023c3c2 | 2021-05-18T19:07:43.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 308 | 5e4fbb871f2b8558d575ceed22734e180bf0d4a4c7c621cefae499c8f246b584 | 2021-05-18T19:09:13.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 309 | c294bfbc5e104457a4dbf248e4ced0eea040e7132662f2de633f80dfbaaeff1a | 2021-05-18T19:10:43.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 310 | fa9b413f5ef03909006fc963360d9b7ea2e825d7f6f0231bafae7a1ed3d62cd4 | 2021-05-18T19:12:13.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 311 | 1e9b32fd3efb99ca3f5a5bba718845c8e4b31a03d8eeb011f73870925dbff1bc | 2021-05-18T19:13:43.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 312 | 239a8f01bf350aa86abda06bdf2738bcda0f4253856df15e90611642842a648b | 2021-05-18T19:15:13.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 313 | df88fbb7ad591eb58fedec767fd4eb6bff03cdf60321ebaff44317799aa85ee6 | 2021-05-18T19:16:43.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 314 | 231e3f620f3d629f580b20be3cf389a6b67e97cfe1d1162cdd4afb826c2069bc | 2021-05-18T19:18:13.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 315 | 4eacb9f7be4497bbfc15f1faffab085ee3d9d2d9530e31b497f321cc9fd65f7b | 2021-05-18T19:19:43.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 316 | 1b82ab78cfc35c86fc70b33c02eae7eec4d5c98e6d0733451146f8b08e735b50 | 2021-05-18T19:21:13.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 317 | 59dec96c87a79de739bf73d3c285e37ed555977a923682afc2ac0f213965c468 | 2021-05-18T19:22:43.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 318 | 67de967d7092de0d9397f03870ab52fba809fe832475ec922c1bf8c3f9e61bae | 2021-05-18T19:24:13.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 319 | c0bd65304589d2af6c8d1b6f275dc2ece8e8aaf293e8d628c38684b40c5f0241 | 2021-05-18T19:25:43.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 320 | fde40bfc0287ca6bd9298ed200fb6a8706f8bb96f4fd1a9a6e9b242cb0985765 | 2021-05-18T19:27:13.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 321 | 896cc5bbba20ddb213efb143080643300376fd9b0d7543cc52abec4f2eec30f0 | 2021-05-18T19:28:43.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 322 | f6d368a2ebd7ca9896f6b107a2aeb4dcf89f70c3b760fe9a218244bdfcd69fe8 | 2021-05-18T19:30:13.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 323 | ec83ca2f4ce144417ecde260250d8d115432906d9e5f51e6573456d945b1b56a | 2021-05-18T19:31:43.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 324 | f8534e2da308ae8bd84aa19ef9940d37a39708e05b0bd92edc822b387da50dfb | 2021-05-18T19:33:13.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 325 | 86d11cffe54a1f86640eb0182a4e9f38706182081030087d246b52548a461146 | 2021-05-18T19:34:43.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 326 | 20fc5fe1d7b10751bcf59fe595ab08cbb59249203c40344bd825c0bdf8b2d77c | 2021-05-18T19:36:13.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 327 | 694c5cafcc17e32a317db2e257fdf3dd4a7e80c34aff0242818c4459c361befc | 2021-05-18T19:37:43.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 328 | 385c07af4904d1122809fbf312c02f117eba738a547a77630fa58200dacc5a9f | 2021-05-18T19:39:13.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 329 | 15684f9c66d0b7fb4e2aaf123cc7d5b4010cc2c4e08971f76fd5d536d1eec9be | 2021-05-18T19:40:43.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 330 | 0f28e2b44d6b4cfbd487db26bb79d79ebe08b68570cac54ab1003fdbd7d0ef13 | 2021-05-18T19:42:13.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 331 | bcff0b223bae1307dddd89cd14c1d15da0c41a09112f8da701dbe23992133c70 | 2021-05-18T19:43:43.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 332 | 76883e90607a12c9edc90dd3512cb0e593bae4f465ace754d77c6e81761606d0 | 2021-05-18T19:45:13.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 333 | 5aabc49947172e091fed37123ae7c30a9a44413b4f2eed2db8d576d709bd9ca4 | 2021-05-18T19:46:43.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 334 | dea88fa445476cf77c9edeb1dcfaae6e3cee28015924a9a6877fb32d8fa7f230 | 2021-05-18T19:48:13.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 335 | c48945a327d9973435d58ef0dba7ceed8c0eb5e1c8165f3aa54697fa30a6464e | 2021-05-18T19:49:43.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 336 | f391022f17bcff5c049f811929d1cae7b13d6aa2be41f0ae0eec568395820cbe | 2021-05-18T19:51:13.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 337 | 8b8c9cbff43282b4c4835e0c364607264b373f2a4a10c8270cc2b76a7007c143 | 2021-05-18T19:52:43.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 338 | 8688872cd0f560bde72c575351857839da73283d421f467ad4b945a397e5be9c | 2021-05-18T19:54:13.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 339 | 7ab10d073158e059ec7c0d9721e4602936e07110e6ede8b2643bf7faebf18124 | 2021-05-18T19:55:43.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 340 | 3d61c4737e897968adb92a43fd53149e100b3787fa564fed451328602464caf2 | 2021-05-18T19:57:13.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 341 | f601d6fbe66acd03f666cd782111391da448d8d8e5824b5d0451632aa95a03de | 2021-05-18T19:58:43.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 342 | 3f5648836e8e9e213a2c75a72882013d3a28f34f1d429c9f363344443d5fc6f8 | 2021-05-18T20:00:13.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 343 | d7ad5ca60d0326f21024fce01b5721a83c9c622c128ee8309d7af783ed5a476c | 2021-05-18T20:01:43.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 344 | b00a63cdb31c33865f170d223ead402102c31696105eacf9a3eb34f116528ad0 | 2021-05-18T20:03:13.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 345 | f0bf218ff3f7cdebfd5929ffa7bae8c6687210b5bffd9f1ea7da8466061b4faf | 2021-05-18T20:04:43.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 346 | ac4f4e427a73857d8d2a9ffcafe4b3928544a4fba1bb52bdef6ea8074f25e2f8 | 2021-05-18T20:06:13.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 347 | e54196380b3a4e25570c2cb0b1dd4a53f104da89fbfdf7f8d5bd90919d35e3b5 | 2021-05-18T20:07:43.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 348 | 940836db66bae2d3ce2169cb3aee14143dffd867c53e2ba48dbd3e4ef8d1c079 | 2021-05-18T20:09:13.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 349 | 9c008841534e85151c41d0378311f22dafab0eddb7a8d45c2c80b0d00b825849 | 2021-05-18T20:10:43.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 350 | 056d87e4e10afca07fb014c25668fac75f894a54b76bd90211f4cdaf3eff3263 | 2021-05-18T20:12:13.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 351 | e8fa082228c3ed51f2b62b0b459b50ac956752b6b1a938411d931d156e7341aa | 2021-05-18T20:13:43.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 352 | a8c5528510f56ec8454dcdc6ce5a9fa0c25ab91cf288f7177f24c8571835c7c7 | 2021-05-18T20:15:13.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 353 | 95c7d12f31b542eac5af0e6516d75baa8d450541aa2ff38a0e7e5bfaeff1518e | 2021-05-18T20:16:43.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 354 | 4979b0f75183af5cfe95a9e7f356f565348abba65fdf024cb69c73bccfc7d9c9 | 2021-05-18T20:18:13.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 355 | ff3444f3d443abdfafc26e374cdb43880e352dbf371d382968bb8f155b2af3d7 | 2021-05-18T20:19:43.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 356 | 5a47734cb998a2980b4b5da3dc75fada07dae6f8b6c1c47304ef8be18e62fdbc | 2021-05-18T20:21:13.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 357 | 56d3a482733469648d6a427eabb227937970f30afb253d643114f536999f37bc | 2021-05-18T20:22:43.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 358 | 2a5f06085fb1610679b64e87e604440263f725bf3b289099c99ae6191fbb761f | 2021-05-18T20:24:13.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 359 | 828e75ff3a979ada914cbfeb4a173f330d6c63f9c1f8f093ff1592ecba4e4a03 | 2021-05-18T20:25:43.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 360 | 7faa2fa10fdbc9a2e9287248f4e564473a5db42d72c5ee1bb5bc41f684eb9fcb | 2021-05-18T20:27:13.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 361 | 957501b22bf1e6b5588cd23cd129df4f2a6765eec704825bc7172b3016b3bcf5 | 2021-05-18T20:28:43.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 362 | 8233c00a9fed9618b2aff9d1a13d908f363784e4c8d418a17b659d8566f0ba02 | 2021-05-18T20:30:13.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 363 | 436155bf2561ef15fb5f4848d026c94331019ae42ce77a793961c577676c0019 | 2021-05-18T20:31:43.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 364 | 0cda6fab24d823416fe51027173ae97077c23d4ab5624070d0f3d1fc52eb657f | 2021-05-18T20:33:13.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 365 | 0086a7599bc2bb474c576a900b3b68b03ec93d5c8f9789f0cf3369bb20bff084 | 2021-05-18T20:34:43.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 366 | 3e4741a978c119d103a9390ddd15b3beb6ee07eedc962dc9a34391ef49cdae62 | 2021-05-18T20:36:13.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 367 | 33187fdd4f16ec221af0c94d84f8ddef9186453a04fe57452823641241b1b80d | 2021-05-18T20:37:43.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 368 | aec20df9f05d78060e3fe1526902310fc0aaa7c13268b6f18d9eb7c17a118a48 | 2021-05-18T20:39:13.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 369 | 3b990f7b14d2c99d45135ca9f0802f2c2caf6195a11486f9b8f5f2a19a4104d4 | 2021-05-18T20:40:43.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 370 | 7bbf7f7a37e7c42ee84a74c8b48242be8fd65c54f391168f6dba094f245ae824 | 2021-05-18T20:42:13.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 371 | 19e55da7cf94ef3850ba62640a7a4d4ee94477789061824c7c2504bcee61502d | 2021-05-18T20:43:43.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 372 | 7862befc34105d5b3b811dadb12d8c3e06d1ab1442cca8d0bcb65d740dea2e5e | 2021-05-18T20:45:13.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 373 | 46f05dcfe53c7d29e47f0ba29eb33de50136a3c412c85cde84b8d670886519b3 | 2021-05-18T20:46:43.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 374 | 7b9e6a473dfea1527712a33254ae5a3c24c7890301d868627fd1e698039fa729 | 2021-05-18T20:48:13.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 375 | 0c0891e2e773d15734457b63f37b9a6a496263cbbe340791581e7cb7c9ef9d42 | 2021-05-18T20:49:43.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 376 | b82a794c9a4348952c49a3acac839551b1c89d8f6fa328565c05f2cc3b84942d | 2021-05-18T20:51:13.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 377 | 5dceee222e41da92ad18ba6b2b9521d37e6e61358c29f92d1e85453cb0497f54 | 2021-05-18T20:52:43.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 378 | 1cddd1471a48f940547bd5922d090039a16d1c8bbc7ea3fa8fb95220fbed8bcd | 2021-05-18T20:54:13.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 379 | 60525fcaf5da8d45c9d648b3aac922891438a3ae1dd00f92f918dedd3a1406b2 | 2021-05-18T20:55:43.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 380 | 5ad75418e99493083abaf4846ebc7f93f56b33527d953483c87b2a4e8e6ed841 | 2021-05-18T20:57:13.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 381 | 969a3b815854d0a36e6091872657bd9f15972cca0d8a0dab3a7e395571faaeaa | 2021-05-18T20:58:43.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 382 | aa18a69b77092461d497241f44fed80a73395167abda443a846db1beaabfc5d0 | 2021-05-18T21:00:13.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 383 | 9e6443ff255ef31eed3c71984ff673f2237adc2dea7eec1ad1f8a985c9182310 | 2021-05-18T21:01:43.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 384 | c433847e6959b954973143bd76f213e4a882a11f9cff6124f4d37cbbd7af2e9b | 2021-05-18T21:03:13.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 385 | 8a495d97e9bca05dfbe686cefeb0b0fe7c2c300bd3587dc9eb293630264b1a50 | 2021-05-18T21:04:43.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 386 | ca7b9787cc5a2ff368795b31348dd403a9462ffb4c6604b068dd5ecfea5f5b81 | 2021-05-18T21:06:13.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 387 | 81415201b75601fbac09ca320d18fe664265344483396766510853292f219f39 | 2021-05-18T21:07:43.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 388 | c6c49a6726b5f5e494eaff2bf2c2a6d886be8c0f98f679ca2cd45811f2003b46 | 2021-05-18T21:09:13.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 389 | 448886e0d070aaa87324d1e652a5490f734319c7c7d9f2b70dd801bc2ed8991f | 2021-05-18T21:10:43.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 390 | 238facea13bda77490323d62fbcab7b0fbb2b47d896bfdf70aa08ce4cbbbe0b8 | 2021-05-18T21:12:13.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 391 | 9cce4c46fb6e5ccbf1ce34f036499eb11ad46357bb7ee25923218a7b216c5573 | 2021-05-18T21:13:43.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 392 | 190b5edbbfcf4133fdf22d970fe683f54ec384048be08a1b69ad387b98d914a5 | 2021-05-18T21:15:13.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 393 | 60ec0afe935806ee71efd66292e8b2beb1ca536f6ce948e218a37cb0bdb3782f | 2021-05-18T21:16:43.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 394 | 8d4711978fdd3f477bb39dfc6447d7c6dc25843ee510623ae1dee880c0566a24 | 2021-05-18T21:18:13.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 395 | 8880c2c158e6558558c55abae69087a5690bf2b21004e610585ab7be7cf6ae2f | 2021-05-18T21:19:43.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 396 | 4242160fc94fc3bd4bffd2afa5da77be45de8fd12d865a160bd877b823c8f548 | 2021-05-18T21:21:13.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 397 | 7245c28bce49c44cbd3d207941a3861b2e2e1246ef549ce864642f4307aa2f50 | 2021-05-18T21:22:43.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 398 | f1ed7963075855267baa642214684f115c342c950560d797d70fb31f8f887fda | 2021-05-18T21:24:13.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 399 | 1492c9f22b8975636a8daad5cd65c38eda7b926bfd420581fcd23fdb2233b369 | 2021-05-18T21:25:43.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 400 | 458322543cc2e22dea83d9de4fc100e66fe7d9298ab3dfcd132f3e2129a2eec5 | 2021-05-18T21:27:13.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 401 | a1c8d549672d4fd9de8763a54f90557bd161d1f8d2270fe94713dcca3bd6bab9 | 2021-05-18T21:28:43.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 402 | d45eafcb97fb0c4f92eee74e550028540140c7e18e222222f685c587fe12669b | 2021-05-18T21:30:13.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 403 | f936d8f44f261ea4970df460322cd68b0f90f6a045b51e07addf1891eec0ad6b | 2021-05-18T21:31:43.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 404 | 63931a5e057d6a079d58f3ac6c9544de6efc9ddac1f0f006e6c79a71e41dc615 | 2021-05-18T21:33:13.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 405 | 1ed20742cae6311782dc64929ccd2db1d4fcbbd7374b5b8994a29d5bc184fa60 | 2021-05-18T21:34:43.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 406 | 5979d268e6eeccfde047ed639cfa418754a7b061abe582cb633250fdfac178d3 | 2021-05-18T21:36:13.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 407 | de1688bfc3ad0b19dba3cce13f70af100803e58e0bdc1520403c5764118ec6a7 | 2021-05-18T21:37:43.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 408 | 3e3bfdc7118138bad856231650916a9423aec865f661df24b0b4359fac2c9895 | 2021-05-18T21:39:13.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 409 | 83edee77ab33b61e614f305e3fd60a8b48a7aa1fb8542dc5f8fc0017b99d8fc4 | 2021-05-18T21:40:43.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 410 | a6bca1a0eb622eb91c89f4d5cf7c9a1b715d82a873849cfdeb9d630b8318d69b | 2021-05-18T21:42:13.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 411 | b3ab4ee99fbcf07e22deb36df345be5a586a33be5a6e21c0b0cbbf4147922bd7 | 2021-05-18T21:43:43.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 412 | 4fa6e4275c49e775444be79cd3720992356f41b142ddd1af18627a0fae094cd0 | 2021-05-18T21:45:13.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 413 | 79e6ae7f9d4af5748b8bb30d9ce5a9e9b6d2eb9a06d342769651a3f0a038bf82 | 2021-05-18T21:46:43.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 414 | 6af98fd5189c085b73959736f968a7906dbcbd854177cba25e474d1154a1c5e1 | 2021-05-18T21:48:13.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 415 | 369918440a47d15bfb19183110850763f5522e00f43ed08bd2fe327b58c07a5a | 2021-05-18T21:49:43.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 416 | 52e2ded88f0fdf30397db54809db950f1706975e2402cf6cf29748343e270726 | 2021-05-18T21:51:13.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 417 | e32535a90239da6e67708d669c934e3526f89159fdfd1eba5f8f9b143fb4d516 | 2021-05-18T21:52:43.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 418 | 94febd8543d078b26e0cf44c803304882df242325ab56bbf0fe3825096eea4ee | 2021-05-18T21:54:13.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 419 | f76ad4cbb3aee2b04791d47bc67f9d5b8d5aff230981ef96f6781083918d0fa2 | 2021-05-18T21:55:43.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 420 | f4b4376b73219ff1af5b80731293877e78286fd673f878000cbe9d7a162a2896 | 2021-05-18T21:57:13.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 421 | b43dc6b1373888e0c4eb2db0b6e9aae2de9ff2d3f142f29c179cd4aae2a8cc87 | 2021-05-18T21:58:43.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 422 | 8a0d2c88d0f6c8651d74116333bb97a25cb4c6fa10da663746f31b05f7870be7 | 2021-05-18T22:00:13.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 423 | e4b2040342291c145c7d635946f44069d5469636cff75388b0a75e62b9a0a616 | 2021-05-18T22:01:43.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 424 | 6af26621eca92babda2df3ebcd2fe269946b3bf208183569258630e64486831d | 2021-05-18T22:03:13.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+| 425 | 594d59b2e61bb18b149ffaac2b27b0efe1854f6795cd3bb96a443c3676d78683 | 2021-05-18T22:04:43.2430864Z | 8af6765ca5a6fa9916e076413192c5da435d92a292b7bcfbb4b2764b9dc58e80 |
+
+
+
+## Events
+
+Event listing contains the data about the events in the index.
+For each block, one or more transactions are listed.
+Transactions are listed by their index.
+For each transaction, we list the events recorded during that transaction - the ID, event type, and any relevant data (e.g. in the case of transfer the account and the amount).
+Events are also ordered by their index within the transaction.
+
+### Notable events referenced in tests
+
+```yaml
+block_id: af528bb047d6cd1400a326bb127d689607a096f5ccd81d8903dfebbac26afb23
+height: 13
+transactions:
+- ID: a9c9ab28ea76b7dbfd1f2666f74348e4188d67cf68248df6634cee3f06adf7b1
+ events:
+ - event: 'AccountCreated: 0x754aed9de6197641'
+ ID: 84db285c44d1422fb75fec6ee522097c1498f86070eb9c977c40b56703344cd1
+ - event: flow.AccountKeyAdded
+ ID: f29833fc5ff8a11a9d08c3cb81b20d4e8c3f825a5fec28bc85c30ac17d4a6750
+ - event: 'FlowToken.TokensWithdrawn: account=0x8c5303eaa26202d6, amount=100.00000000'
+ ID: ba24b1dc1d90f0dcd614c75f58cd37c9bc69ef3fe7ed605624067b72b3eb45cd
+ - event: 'FlowToken.TokensDeposited: account=0x754aed9de6197641, amount=100.00000000'
+ ID: 8e201802501b38118f2d082557fdf321c377ac618b29f51b0721632bacfe2b33
+
+
+block_id: 810c9d25535107ba8729b1f26af2552e63d7b38b1e4cb8c848498faea1354cbd
+height: 44
+transactions:
+- ID: d5c18baf6c8d11f0693e71dbb951c4856d4f25a456f4d5285a75fd73af39161c
+ events:
+ - event: 'FlowToken.TokensWithdrawn: account=0x754aed9de6197641, amount=0.00000001'
+ ID: c228d259e99e977e737679f3d783781f20c214fa5d18fa7726d0422b463d54bd
+ - event: 'FlowToken.TokensDeposited: account=0x631e88ae7f1d7c20, amount=0.00000001'
+ ID: 57e5527b74c4939b3683af93ebd78ea116baef17222f42ea34520da82bed0ee0
+
+
+block_id: ad5f39a9f8d95ba4bceef55a9ca753bb797dbe847a8e80ea784139ac28d9833c
+height: 165
+transactions:
+- ID: 23c486cfd54bca7138b519203322327bf46e43a780a237d1c5bb0a82f0a06c1d
+ events:
+ - event: 'AccountCreated: 0x72157877737ce077'
+ ID: e5066494dac0521ba1f42cc979f71412f8cd3fbd803b5bcdae3a9b7baf47b1e0
+ - event: flow.AccountKeyAdded
+ ID: f93be0445a858ddf9846818116350137c9f75166fd70b00b5044bfb1b3760b73
+ - event: 'FlowToken.TokensWithdrawn: account=0x8c5303eaa26202d6, amount=100.00000000'
+ ID: d3e50a104299316260a89d7340d4e914d88a002e7e0a0ba5a17e9018c8ca9325
+ - event: 'FlowToken.TokensDeposited: account=0x72157877737ce077, amount=100.00000000'
+ ID: 561193a787fe9b6220af92fdc6755c2d5a03848187608d8b34084a5c151cc291
+- ID: 3d6922d6c6fd161a76cec23b11067f22cac6409a49b28b905989db64f5cb05a5
+ events:
+ - event: 'FlowToken.TokensWithdrawn: account=0x89c61aa64423504c, amount=0.00000001'
+ ID: 335840041550db48d8c39b229db2f79c412df7016b30542f5685ebc8ed902f2b
+ - event: 'FlowToken.TokensDeposited: account=0x82ec283f88a62e65, amount=0.00000001'
+ ID: 595c713ac270311e0c204f57e26861b9e8fb111f6545a8c1fc8409e483298e03
+
+block_id: 0b11ddfc1d324ee830f27648166d1e52c5868096f43f840f7bd39a0be7346a11
+height: 181
+transactions:
+- ID: 780bafaf4721ca4270986ea51e659951a8912c2eb99fb1bfedeb753b023cd4d9
+ events:
+ - event: 'FlowToken.TokensWithdrawn: account=0x668b91e2995c2eba, amount=0.00000001'
+ ID: c8a754050ba241d4f361316ee3b6f95c3ef376b16ad2273dbc4e92e75c3f490d
+ - event: 'FlowToken.TokensDeposited: account=0x89c61aa64423504c, amount=0.00000001'
+ ID: fa847a3019da089a2d1aa0091c1fd6622c2a5f0d4f756b57d38935ea960270d9
+
+```
+
+### Full event listing
+
+
+ Show all
+
+```yaml
+block_id: af528bb047d6cd1400a326bb127d689607a096f5ccd81d8903dfebbac26afb23
+height: 13
+transactions:
+- ID: a9c9ab28ea76b7dbfd1f2666f74348e4188d67cf68248df6634cee3f06adf7b1
+ events:
+ - event: 'AccountCreated: 0x754aed9de6197641'
+ ID: 84db285c44d1422fb75fec6ee522097c1498f86070eb9c977c40b56703344cd1
+ - event: flow.AccountKeyAdded
+ ID: f29833fc5ff8a11a9d08c3cb81b20d4e8c3f825a5fec28bc85c30ac17d4a6750
+ - event: 'FlowToken.TokensWithdrawn: account=0x8c5303eaa26202d6, amount=100.00000000'
+ ID: ba24b1dc1d90f0dcd614c75f58cd37c9bc69ef3fe7ed605624067b72b3eb45cd
+ - event: 'FlowToken.TokensDeposited: account=0x754aed9de6197641, amount=100.00000000'
+ ID: 8e201802501b38118f2d082557fdf321c377ac618b29f51b0721632bacfe2b33
+
+
+block_id: 57287453ba50c26168630bd364ee1d6d5a62cb47e2909d8f41f17c4e4aa401d9
+height: 26
+transactions:
+- ID: 23a69cee0d29eaedb9b81d8b12ce3bcb2c1ec902e26fd6ab2765d46f9593efa1
+ events:
+ - event: 'AccountCreated: 0x631e88ae7f1d7c20'
+ ID: 236c28c5814a3fdde967e29234104c61e5600b05a76e8abeca07f9a36def5aa6
+ - event: flow.AccountKeyAdded
+ ID: 4112b5c2b7736f1ca5cabd27bb46d944586b517047bf221a7cb3fcd79838bba7
+ - event: 'FlowToken.TokensWithdrawn: account=0x8c5303eaa26202d6, amount=100.00000000'
+ ID: 97d1a5d66459deb135dcac800e2b71760296325529fd56773a871667d0c9bde9
+ - event: 'FlowToken.TokensDeposited: account=0x631e88ae7f1d7c20, amount=100.00000000'
+ ID: 16aa8a6c0f9f6043fb339c733c29ee7daf7a77081c2911d2992a9dc282ae68da
+
+
+block_id: 44a30ade9d4f316b2831a7a84a240cb44c796dc03cc5c657ecb00ae947cec2e9
+height: 38
+transactions:
+- ID: 602dd6b7fad80b0e6869eaafd55625faa16341f09027dc925a8e8cef267e5683
+ events:
+ - event: 'AccountCreated: 0x877931736ee77cff'
+ ID: 2e14775c6140a5bb119b4ee236b0bc91572e9a15014baf716e6d2669ae1cf955
+ - event: flow.AccountKeyAdded
+ ID: 2f0301ed28eea96f26e9eac825d6ba53bc58399150cfe7792f37ae176b5d4e56
+ - event: 'FlowToken.TokensWithdrawn: account=0x8c5303eaa26202d6, amount=100.00000000'
+ ID: 0f320caf400f5f2acaec89e7fa0e8add951c9b51fdf8353d98dc0794eb2a7491
+ - event: 'FlowToken.TokensDeposited: account=0x877931736ee77cff, amount=100.00000000'
+ ID: 5558795158ed93e0b43b3a16ccf335150985a7bcb5f5e59b90aac8e86e0e4cc3
+
+
+block_id: 810c9d25535107ba8729b1f26af2552e63d7b38b1e4cb8c848498faea1354cbd
+height: 44
+transactions:
+- ID: d5c18baf6c8d11f0693e71dbb951c4856d4f25a456f4d5285a75fd73af39161c
+ events:
+ - event: 'FlowToken.TokensWithdrawn: account=0x754aed9de6197641, amount=0.00000001'
+ ID: c228d259e99e977e737679f3d783781f20c214fa5d18fa7726d0422b463d54bd
+ - event: 'FlowToken.TokensDeposited: account=0x631e88ae7f1d7c20, amount=0.00000001'
+ ID: 57e5527b74c4939b3683af93ebd78ea116baef17222f42ea34520da82bed0ee0
+
+
+block_id: d99888d47dc326fed91087796865316ac71863616f38fa0f735bf1dfab1dc1df
+height: 50
+transactions:
+- ID: fef63df18a5b986ced4abc3c1944674d313a6a1798ae9b8193f0dc3ad6cbcbc2
+ events:
+ - event: 'AccountCreated: 0x94b84d0c11a22404'
+ ID: d35086a853e9bdcf756b365d6f6b4e43d10c47ce6cc8406d25d502ce73d36261
+ - event: flow.AccountKeyAdded
+ ID: f4a04b95f5ee4b7e56330a3a6cca8fdf682992398a1f531a1cbb68ebb44da30a
+ - event: 'FlowToken.TokensWithdrawn: account=0x8c5303eaa26202d6, amount=100.00000000'
+ ID: b76117c5e47996e89d68a8d7c1c81ca0eaaf6fb902216b342b3f2032f9613932
+ - event: 'FlowToken.TokensDeposited: account=0x94b84d0c11a22404, amount=100.00000000'
+ ID: 08c3417a88210fbb810adcf87dc10946c4b59088ec07c710a75ff4f5c6003411
+
+
+block_id: 19cf406081a474cf351c4d96a6a199f418f3d4e553fdc5e5bdfee05c00fa4e12
+height: 54
+transactions:
+- ID: 40dc5b6476698dcc8d9804fe29873c3e767cde164060761c871674d7f967b979
+ events:
+ - event: 'FlowToken.TokensWithdrawn: account=0x877931736ee77cff, amount=0.00000001'
+ ID: d979fd3c9edf7a470a650f224f8bbeeb7fcebc217d7d8ef2878715a26b32d6e2
+ - event: 'FlowToken.TokensDeposited: account=0x631e88ae7f1d7c20, amount=0.00000001'
+ ID: c80b898f33b6a21d17e1ac58453c54570ae20e81598975cab0be918823743567
+
+
+block_id: 04f77122a2251cfaaa8ef44ed87090871b78c489dda5bf0d7520776ea0ae9254
+height: 58
+transactions:
+- ID: 920dd9c56a4584dfa258407f1720d0fce5248f9ae0e1047160fb183669f0831b
+ events:
+ - event: 'FlowToken.TokensWithdrawn: account=0x631e88ae7f1d7c20, amount=0.00000001'
+ ID: dd85ef5561800c63345e47ce25f29681154ce7b9b69d80ad03eb3f64131a42ff
+ - event: 'FlowToken.TokensDeposited: account=0x754aed9de6197641, amount=0.00000001'
+ ID: 94f672ff8ca05dca0bdadbcd58238d1f6e3806ae4dec18547b1c906d205249c4
+
+
+block_id: bb884e46baa348dde4f6368e5c85ee6392c29ab5230f2f88a35f48bae798145e
+height: 66
+transactions:
+- ID: cefdf24de288f148d286574e34e78b254db064ac9b41e62435283f2824ba994b
+ events:
+ - event: 'FlowToken.TokensWithdrawn: account=0x94b84d0c11a22404, amount=0.00000001'
+ ID: 247fe5f4421e042873d643e02a3ff90423269268ed117500838955537cb6be78
+ - event: 'FlowToken.TokensDeposited: account=0x754aed9de6197641, amount=0.00000001'
+ ID: 9c6375b739ee99738761586422e18d3d19e3b50f9c2dc38b51f92de52ff7d2d7
+- ID: 724e5e5db3d6fb3a32270ce2d223b600c06af7c4ae619c57127d47a8d59c0095
+ events:
+ - event: 'AccountCreated: 0x70dff4d1005824db'
+ ID: 524b983f847c9c1ea4875b9aaca4ffd49b21cf35883cdab9a279b801988fc9af
+ - event: flow.AccountKeyAdded
+ ID: 791f05db1b848d6da18a60f37e696e9629b80fb5d772c26c169376c9358c20a5
+ - event: 'FlowToken.TokensWithdrawn: account=0x8c5303eaa26202d6, amount=100.00000000'
+ ID: 24c95c867f1d7b9a9f11aaaa88505c2d2086afb5943fe1f4e6733fb4fcc0a9ad
+ - event: 'FlowToken.TokensDeposited: account=0x70dff4d1005824db, amount=100.00000000'
+ ID: 29e9de910f5ceb187ec0fd4374142e0d99dda6f1d3e9584a1f9f148fd2e88317
+
+
+block_id: 92aab4fd2e792d3577089f5bef118064be2d2445225f489c19e30dec3631152a
+height: 67
+transactions:
+- ID: 0aa3671986d5f8e06bf96a47867e0d63a9eea268678f27e3879e1af1a808cd46
+ events:
+ - event: 'FlowToken.TokensWithdrawn: account=0x754aed9de6197641, amount=0.00000001'
+ ID: 3746e91323d6ee1ea7d65a4b83b2742c66eee4175fedce82a529a846b505736d
+ - event: 'FlowToken.TokensDeposited: account=0x877931736ee77cff, amount=0.00000001'
+ ID: d13d19afd9b12c9af173ba6d56b62ca73e0f7a5d9d2c56c583d1fa2c3b709d9d
+
+
+block_id: 0faca24e18619e47e09f8ae7c69986039661987d8f33224d55006d2436eaa2fc
+height: 71
+transactions:
+- ID: ccaea1ba99ced9e6e9214573831259e52bc328362867be7c398c764cc45d51ef
+ events:
+ - event: 'FlowToken.TokensWithdrawn: account=0x877931736ee77cff, amount=0.00000001'
+ ID: 70c215641f59ee6d311ace99d3bad6416541d2344a4fad64719053009b73c78f
+ - event: 'FlowToken.TokensDeposited: account=0x631e88ae7f1d7c20, amount=0.00000001'
+ ID: 6489be6c14e156cd1d790553994cd7c40cae9a8297021e14838aa20c54a3c02f
+
+
+block_id: 7c624da4f661545ce412e06a1705788bcfb6c0abdf2ab24a31831243d7e80468
+height: 78
+transactions:
+- ID: 1c62c61d26c04278f8492e5daf5a08be2c443a41cf42d1260284d9ed8dc4d7cf
+ events:
+ - event: 'AccountCreated: 0x668b91e2995c2eba'
+ ID: 190d03e52cbff1fed3255d7b0f1753fd11368baf2faa6d83687372f390c5c088
+ - event: flow.AccountKeyAdded
+ ID: b300b93490d7a98f17058094abe1e284b95b1bf80b4dc437949800ba608c1455
+ - event: 'FlowToken.TokensWithdrawn: account=0x8c5303eaa26202d6, amount=100.00000000'
+ ID: 40eb0bf19600fbe7b6e6ed7666fcb1978f9b74d6f7da4d5b6438f79e28b996f3
+ - event: 'FlowToken.TokensDeposited: account=0x668b91e2995c2eba, amount=100.00000000'
+ ID: 2bb64cb932b61ae0eb6751a7b4ae3f3d83a91bf195148afb8aac4353d87b02a5
+
+
+block_id: 83fc62bd6da9c545ab185782ba7a284f71d8f000de296825ae4af2c77cc05e41
+height: 80
+transactions:
+- ID: a9fc9f58665124235a4636316edb5bc605444f986374369ac1c852acec53d024
+ events:
+ - event: 'FlowToken.TokensWithdrawn: account=0x94b84d0c11a22404, amount=0.00000001'
+ ID: 9ea8ad604dfbb66721ec036075cffc648e9c8b2ffba1fb22ef332ef5814f214c
+ - event: 'FlowToken.TokensDeposited: account=0x631e88ae7f1d7c20, amount=0.00000001'
+ ID: ef1bdb89c1f4b07a1bc9705b1e14980a7c0624fd4488c237a5c33d9fdaa0b9d0
+
+
+block_id: 3a34259eab11e7e2e8b1cf24c7be801db5c01a282b56c114d46cba8646b9f733
+height: 82
+transactions:
+- ID: e58ee839b415f8114d28faecd02c0d05958eeaede651dbaf1d319728f6133724
+ events:
+ - event: 'FlowToken.TokensWithdrawn: account=0x631e88ae7f1d7c20, amount=0.00000001'
+ ID: c8e8b4921e205c2bafa54f42c992794ce5ffb533e01d4095fe6343cbaed5460a
+ - event: 'FlowToken.TokensDeposited: account=0x754aed9de6197641, amount=0.00000001'
+ ID: 33ff96eeecd37ab5782a36bb8335815e61bc5714c04a049f6d925074d40f9508
+
+
+block_id: 7fe16f21f7f81add1b6ff98d8e7cdc654d023b6f20485e8b187b156dfa17b6df
+height: 85
+transactions:
+- ID: 7e643bd09c8ec65efbb1de2a53bb96493992ccc73d31bd2b512686f5272264be
+ events:
+ - event: 'FlowToken.TokensWithdrawn: account=0x877931736ee77cff, amount=0.00000001'
+ ID: b9db6bf8895a22ed9fb87cb9790748eec9c74803a12e06e63db34edd6c98afd0
+ - event: 'FlowToken.TokensDeposited: account=0x70dff4d1005824db, amount=0.00000001'
+ ID: e5666125fbb1202507785f9c7582cf7e28a507ddacf57c819ae27f6d28916c29
+
+
+block_id: 93f6c6062fff880bc55f4a9b1b656026b1c5025e16b70db75a54682707152749
+height: 88
+transactions:
+- ID: 849c2daa1637921607bf645616ef83d4ea052c24147181eba985f858ab6806cd
+ events:
+ - event: 'FlowToken.TokensWithdrawn: account=0x754aed9de6197641, amount=0.00000001'
+ ID: 846da9fe4078fe6cf15ce22adc1b11c2c683b04b34635d0eddfac5452ae7a544
+ - event: 'FlowToken.TokensDeposited: account=0x70dff4d1005824db, amount=0.00000001'
+ ID: d6dbf899df1288f571a20f3b0c17ee5bbe34a4d32ce87ec5c7db6a6a95456675
+
+
+block_id: 75cbf0019f0b4879c96e57da042ef2cac2d86bab3b98c51651a38c323705e5db
+height: 91
+transactions:
+- ID: c9cafeb247d30424da537ab39fbb0ecbff6f3cd60590262ab839206eb26ce791
+ events:
+ - event: 'AccountCreated: 0x82ec283f88a62e65'
+ ID: 3540c4520e25d12d5c45f689786b940435f6c9f4eb5ec773c33de85e6067f4d7
+ - event: flow.AccountKeyAdded
+ ID: 57693bf880a1f0975c0b11c3c2b79fa0c80e88ac7aabd148b667ec90f4992e27
+ - event: 'FlowToken.TokensWithdrawn: account=0x8c5303eaa26202d6, amount=100.00000000'
+ ID: 17886e7927f1ab5a6bdbb7ed3d24ecaffdee19bb4fc231a86cd4e01390ac115c
+ - event: 'FlowToken.TokensDeposited: account=0x82ec283f88a62e65, amount=100.00000000'
+ ID: 63dd77e7647c3aabbb9a87646aa85c33b10b82a43a26a1b8cbd7e83dffee4b24
+
+
+block_id: 2469a9561373b2c5d5b18bd2a04c25dc2b4aedc9a95002dca98457cc248a00fc
+height: 93
+transactions:
+- ID: f0f9672e3c371b5fb9e137537bd6c427c2d2ad1cae13b746ea7909398ac87bac
+ events:
+ - event: 'FlowToken.TokensWithdrawn: account=0x70dff4d1005824db, amount=0.00000001'
+ ID: 5585ccf5eba98bb19c0a2163548439443ebb5fdd019edc3eaebb8f91e27aa8d6
+ - event: 'FlowToken.TokensDeposited: account=0x668b91e2995c2eba, amount=0.00000001'
+ ID: 00bc682d58135e13e91be9780dc07d70f156cf2cbbc5d7ea059d72adcbc64108
+
+
+block_id: d147a014c55e5916dc00b377684967c45be806ce981a414bce510521809a2dc3
+height: 95
+transactions:
+- ID: 65fca0c93c05207a5248a47157fef86fb7864d6eb86c00c9d4aefe90c7820106
+ events:
+ - event: 'FlowToken.TokensWithdrawn: account=0x668b91e2995c2eba, amount=0.00000001'
+ ID: 360723794fa08131f21e644e0cf0093128bbff49e956e768ec99600d3a28a789
+ - event: 'FlowToken.TokensDeposited: account=0x631e88ae7f1d7c20, amount=0.00000001'
+ ID: 88822cb8b08c3e59f59149d94c8aad821ab26964de3d6c8e879c6ec6a02e903b
+
+
+block_id: 04d7c525ff65f0e5fdfe4a628637e3294f2a37b2ea85c252060d1c2fb9336940
+height: 97
+transactions:
+- ID: b8322cd9ed9010aeba9c6448c5bf36029214b190ac5016e0c3f293deb5bf02f9
+ events:
+ - event: 'FlowToken.TokensWithdrawn: account=0x631e88ae7f1d7c20, amount=0.00000001'
+ ID: 3cdecec7a7f76a3e3a00111529f9075a325575f1b5c0d541f54f3c53aec7f915
+ - event: 'FlowToken.TokensDeposited: account=0x877931736ee77cff, amount=0.00000001'
+ ID: ec65ed8b0fea0d13d1af0c9ee0b7a2079b93c6f9207bb5f5e063a877aa629c86
+
+
+block_id: 64f314baa78d4bb334b950ff265c012cae033cb42d92c2adc6650616371d14fa
+height: 100
+transactions:
+- ID: e2b7eccd579ffd3e5440737a3c0b406c542e93d56c54a5b667112740d56ede38
+ events:
+ - event: 'FlowToken.TokensWithdrawn: account=0x877931736ee77cff, amount=0.00000001'
+ ID: 38b61f96d042fff1a866217fea80dcc255a87b4cbc05a6a960efaace541ef3d0
+ - event: 'FlowToken.TokensDeposited: account=0x94b84d0c11a22404, amount=0.00000001'
+ ID: 62bad8738c4cb8bc76f757a494733020744f9400df921dfc602b16defeb9d5dd
+
+
+block_id: 61d354e63f93ca84689a0b8ffe891dbb87a85e8584f406a73ffc1a6e4b57e80d
+height: 102
+transactions:
+- ID: d88a6e7207e7c322d0e5188fb0326a1ba741a1372213bfb3e190de856de3ea62
+ events:
+ - event: 'FlowToken.TokensWithdrawn: account=0x754aed9de6197641, amount=0.00000001'
+ ID: 7122f913d7eb2c59b717560461c890fdcd8ea8ddfc69a581bedff7c28abb32f0
+ - event: 'FlowToken.TokensDeposited: account=0x94b84d0c11a22404, amount=0.00000001'
+ ID: 046507d41f93692f722bb9df957c14f30803331bd30aac905918ad8574dae657
+
+
+block_id: a0c534a0071db00b184d3ad6045f9ee1c5e8dcfeebd4deecd81753c80f355151
+height: 104
+transactions:
+- ID: e0b836de4bbe322f2f3258e8d3f7c5fe2cf839f3eca68947f5e7fcb0660423db
+ events:
+ - event: 'AccountCreated: 0x6da1a37b55d95093'
+ ID: 0696515319c66e677b91c457a0ee4d4c8774a23bb38b22dcc125b0c057f825eb
+ - event: flow.AccountKeyAdded
+ ID: ca39a505fde25240a7230c3f5e0391e0112e692ffdca6cd59c50a8d7670f8614
+ - event: 'FlowToken.TokensWithdrawn: account=0x8c5303eaa26202d6, amount=100.00000000'
+ ID: d04ed5f2b0fc3ecac146cc440879326a318491688df8ee0fe69793482dfa7368
+ - event: 'FlowToken.TokensDeposited: account=0x6da1a37b55d95093, amount=100.00000000'
+ ID: 992efd75cf9e5e25b0a4b2d58d84f89189b9409b0aaea1fd9f9cf2b281d38645
+
+
+block_id: 1f269f0f45cd2e368e82902d96247113b74da86f6205adf1fd8cf2365418d275
+height: 106
+transactions:
+- ID: 071e5810f1c8c934aec260f7847400af8f77607ed27ecc02668d7bb2c287c683
+ events:
+ - event: 'FlowToken.TokensWithdrawn: account=0x94b84d0c11a22404, amount=0.00000001'
+ ID: 76610a80740d95e0b03b04eb2d30c4f2d00b746954838d1196e373c65b7fec12
+ - event: 'FlowToken.TokensDeposited: account=0x82ec283f88a62e65, amount=0.00000001'
+ ID: 1e4e6ee3160637fb98f26c331010553e06434f78199052126728d6e787d37d07
+
+
+block_id: d752a3b69c2dce554e0fde872a9faf54089ef9d8d2da32aaddacc9d176718587
+height: 108
+transactions:
+- ID: 96c7382122436bd6f2f64451678f751992260dab0cc585948c0143b7a08659ce
+ events:
+ - event: 'FlowToken.TokensWithdrawn: account=0x70dff4d1005824db, amount=0.00000001'
+ ID: e02c621f523b6fad4621173b263f85d903eaf523239e9d28546eb5c01e43c7ba
+ - event: 'FlowToken.TokensDeposited: account=0x82ec283f88a62e65, amount=0.00000001'
+ ID: 12263ffb126d6db142162f95079e6a4e87d371c5619c87a38107b29c71ae2ec4
+
+
+block_id: 47f4604d3ae1acf79aed4277020812cc9465948e9ec93b3c0dcafbe556214f05
+height: 109
+transactions:
+- ID: ff0db565c74ac538f0c03f1f8ad2508cd816f375231b2c02151a090c85055900
+ events:
+ - event: 'FlowToken.TokensWithdrawn: account=0x631e88ae7f1d7c20, amount=0.00000001'
+ ID: 69e3a9f48e3ac9452fbe3ae73f88746387c1e8e4de1b9a709fb7383764b3c1c8
+ - event: 'FlowToken.TokensDeposited: account=0x82ec283f88a62e65, amount=0.00000001'
+ ID: 606821728e4cbcaeabc86d4186e3a44666430a3219923e12dc650fa0c8d7e942
+
+
+block_id: 13b0873ad6008b7cc09fcd4c13d10d81e36675d6a79dae642ca614b2908758fd
+height: 111
+transactions:
+- ID: 9bd0136bbef3d8acb3344eaec795a33a9d94a4bf16dae0c709404888d279de7e
+ events:
+ - event: 'FlowToken.TokensWithdrawn: account=0x668b91e2995c2eba, amount=0.00000001'
+ ID: 4a3190e38e578c043900eddf96354537f78312df52bd0a557fad59a0d5f430f6
+ - event: 'FlowToken.TokensDeposited: account=0x82ec283f88a62e65, amount=0.00000001'
+ ID: 9b5ee0f9f811844781bcb59d6a331e54ad8d07a7f2952b836f3b1a5341cbb5d0
+
+
+block_id: 7ebf21ec64b9be38256f92611f721a18029c7a19c829236be843e5ccf63e17d2
+height: 113
+transactions:
+- ID: 6d8baf21faeafb231fb1f4b514f3f16edaa51b8fb8232b03c60391c38819f1c4
+ events:
+ - event: 'FlowToken.TokensWithdrawn: account=0x82ec283f88a62e65, amount=0.00000001'
+ ID: 4d93c57bd56acb0abf3b8eda96e44aeda5f722d615d221b9b0074b3b75dad518
+ - event: 'FlowToken.TokensDeposited: account=0x877931736ee77cff, amount=0.00000001'
+ ID: 6b1c02f55ba2e874c767dc8473caebe14ba72efe409f624cecf079e4cee84a9b
+
+
+block_id: aee77e983b2034a7b48574c7d8039d36e5bc0720609fe365612f0f6f4481aafa
+height: 115
+transactions:
+- ID: dfde66b4e75baabec53698ee10b404951ecd7d862050861db990ffea3b059b2b
+ events:
+ - event: 'FlowToken.TokensWithdrawn: account=0x877931736ee77cff, amount=0.00000001'
+ ID: bdc6713c0c0dda7a4b42427b66563acf6b4261740aa7d384cbeff733d563e3dd
+ - event: 'FlowToken.TokensDeposited: account=0x754aed9de6197641, amount=0.00000001'
+ ID: 8f05cbdfe7a5c8b69552e97b506c63b6e16ba488dfcba4e6fd8c00943c0fccab
+
+
+block_id: 30c8ddabe351a2d05076bbb5bf6417e8c9c8b1bcab45e52df43d026bad8bc2bb
+height: 116
+transactions:
+- ID: 4fdf53b42bde23b9f82113fcd6378682a1ae9ad544793b2b0e9e0c2a2854f255
+ events:
+ - event: 'AccountCreated: 0x89c61aa64423504c'
+ ID: ae785971031d2ccc40bdece59ee8cac8adb7bc3048002ac0aa2e912ec49e0ea8
+ - event: flow.AccountKeyAdded
+ ID: 6b81aceba13a40b0afde5b37ed57bf9645138e49da586a5d3d99eba6f561e6a4
+ - event: 'FlowToken.TokensWithdrawn: account=0x8c5303eaa26202d6, amount=100.00000000'
+ ID: 6863f208d727f29a3751a14bd36a36dd253833ab3115ace5ea01ec6e5ac7a348
+ - event: 'FlowToken.TokensDeposited: account=0x89c61aa64423504c, amount=100.00000000'
+ ID: 7c456f9891c9b982b398b383c9804f127907a45f814a109ce04b6c42ecc1541e
+
+
+block_id: 9ae2bf97fb16f2bfdc71bee44eb6c3a926c65166b4468bd5271209ee409a8e9b
+height: 117
+transactions:
+- ID: 4bc78c15c95172a30f872e8f11f5921dddce3598189a094bea1d7a7bac350ad7
+ events:
+ - event: 'FlowToken.TokensWithdrawn: account=0x6da1a37b55d95093, amount=0.00000001'
+ ID: a343ad795eb433278ddd40b7fe3f5e0c071faf96f27e97ae985331557fd6428f
+ - event: 'FlowToken.TokensDeposited: account=0x754aed9de6197641, amount=0.00000001'
+ ID: 0e66629540d8003ae3d80d8b0c62888d8dff4eb341f6e7aa5ba79031c0d1acd6
+
+
+
+block_id: 15a990fb9f638812822b43570bc381a59b58366d378765e6de94e4ecc2c8e45a
+height: 120
+transactions:
+- ID: 9f09d2c473a1a7e7a0161c001f153121f1903381699a62f03b0c82c3f14e612d
+ events:
+ - event: 'FlowToken.TokensWithdrawn: account=0x754aed9de6197641, amount=0.00000001'
+ ID: d3862aae3230ac4003c8bef996bd850231b983fd13fa40af238e33e610003b39
+ - event: 'FlowToken.TokensDeposited: account=0x94b84d0c11a22404, amount=0.00000001'
+ ID: 3ddcb68282109cd48711dd72f88b34f747fd46a54055dfc668b19c0702bd7651
+
+
+block_id: ed97709859b7f3c20b893e8ba55d83450b5c8c26d1fccec31014f282443bdf6b
+height: 122
+transactions:
+- ID: ffae807433835481fca2c16ac0182b97fc9433373e846e7070e7931fd6e82c4b
+ events:
+ - event: 'FlowToken.TokensWithdrawn: account=0x94b84d0c11a22404, amount=0.00000001'
+ ID: c366a266e238d2793ecaecc72fa15b3b85c8433622edb380541a436f9edf0754
+ - event: 'FlowToken.TokensDeposited: account=0x70dff4d1005824db, amount=0.00000001'
+ ID: 217686c74d95417b4adc2ae4aa772b35ac43ed06e4e46fe1af50c41672163820
+
+
+block_id: b6cc6962673eb7a8a1f04e7de75f6943fd41c1158002133c0641853209fb1e45
+height: 123
+transactions:
+- ID: 96e046c339accdc4d6e047973c2e6565c7411f53b2f8820b3d326722a65e094f
+ events:
+ - event: 'FlowToken.TokensWithdrawn: account=0x668b91e2995c2eba, amount=0.00000001'
+ ID: ad5d73d17481527e12aa90fbcf8bd816865f0878ab5a93afc7d8010805b3becc
+ - event: 'FlowToken.TokensDeposited: account=0x631e88ae7f1d7c20, amount=0.00000001'
+ ID: 8d619a735b0da4a4638ec5c3700933f70f53c23f2e00d46b114d9af76a4ddf29
+
+
+block_id: e9b02a0cf5ae5efd4db210f05b47ef22b4cf28bbfab3a062741b3201cc4d5c7b
+height: 125
+transactions:
+- ID: 1d70b649b9c6620ee4893ca36872a0be3d9c2c56b251325de006f2613c579716
+ events:
+ - event: 'FlowToken.TokensWithdrawn: account=0x70dff4d1005824db, amount=0.00000001'
+ ID: 51eadd678bcd9265af2bd3f1a469ccded99c84c2c1220dd786ad1fbabcc17b2a
+ - event: 'FlowToken.TokensDeposited: account=0x631e88ae7f1d7c20, amount=0.00000001'
+ ID: 7f24b1b35713eeb9a47e21fbb41ca0e0c2f4a5477bac93374597b18ff17e2bfd
+
+
+block_id: 9782cfc780f1f2972e6801b8b1743a0c7bfaaabeb5a79d3500dc81f4aeb233e3
+height: 127
+transactions:
+- ID: 80b23c718a43a784ebae87901fb362a63695bc4d878517fa928e761060aa0846
+ events:
+ - event: 'FlowToken.TokensWithdrawn: account=0x631e88ae7f1d7c20, amount=0.00000001'
+ ID: 34482aa08fae1380ba4a801b09d55746967641cac00982c56e1611f1f3a411a9
+ - event: 'FlowToken.TokensDeposited: account=0x82ec283f88a62e65, amount=0.00000001'
+ ID: 4a06d3ec8003d99a2d478154c47d6193d021bf418aa155576609c7a44c8f5f70
+
+
+block_id: 2c35191bc76956977c2f3d474b258ff716ad10c08ad3987fcddabaafbe841a6e
+height: 128
+transactions:
+- ID: 8aeb5753ac9df279e7884ec3da87a70e511cd82e88fcac5f53e12e97231b58f4
+ events:
+ - event: 'FlowToken.TokensWithdrawn: account=0x877931736ee77cff, amount=0.00000001'
+ ID: f2d0cb4e5ead4e9b50f87532805de1e8ac4951669f06b948e109691b182017d9
+ - event: 'FlowToken.TokensDeposited: account=0x82ec283f88a62e65, amount=0.00000001'
+ ID: e05d0224025bf3b767e1fd84f4a70de455243a8fada69e854fe1e55e0e642630
+- ID: ae40dcefad663e79d31592f104c0c6b707d6df60b57358c9fefb0cb527f3c5e2
+ events:
+ - event: 'AccountCreated: 0x9f927f95dd275a2d'
+ ID: f7995f7a2bdca8cfbb81b21feff9f4ef48a5ecf97a1b27630bd1e7b033bd5e95
+ - event: flow.AccountKeyAdded
+ ID: abd34dd5ff6ab41f8f4ca426c9e4735c72b25a50d01f480549122a6da5687a9e
+ - event: 'FlowToken.TokensWithdrawn: account=0x8c5303eaa26202d6, amount=100.00000000'
+ ID: 0b81b595a9c6e655dacfe26629964bb3fab630578a2809e0fd63b985b7a6519b
+ - event: 'FlowToken.TokensDeposited: account=0x9f927f95dd275a2d, amount=100.00000000'
+ ID: 1d433c81eb9c2dc65c256a391a3244771ce690f7b1356be4db7a589bf89c4355
+
+
+block_id: d157384a46ee60e7a5dea6a1e458db7d53a18bba76dfcd8d0424f9ff92902854
+height: 130
+transactions:
+- ID: dd62321ba3a8f0da21bd81ea08aa69f76a844392f1d7f2e65762c7985bc1c031
+ events:
+ - event: 'FlowToken.TokensWithdrawn: account=0x6da1a37b55d95093, amount=0.00000001'
+ ID: 691725357869680f94c8c678d8695e2f7b0ab49905a45ebcece4202ef5739b25
+ - event: 'FlowToken.TokensDeposited: account=0x89c61aa64423504c, amount=0.00000001'
+ ID: 6ebf74eb97fee0fd581033614b8161c8c901e2edfeecb4368d80d693e78cd026
+
+
+block_id: 0a4891afe90bad3e12388612df7dda21722db1522dd638929c3ffb3b357fc14b
+height: 131
+transactions:
+- ID: 19143c29f24bf837518ea9c307d3959481c23a1156bbf68c171865078cb5a87d
+ events:
+ - event: 'FlowToken.TokensWithdrawn: account=0x82ec283f88a62e65, amount=0.00000001'
+ ID: 8438ee7e1f9b54cb864c7b5c958d663ed40929f13cf4f0577b40748235b5b8df
+ - event: 'FlowToken.TokensDeposited: account=0x89c61aa64423504c, amount=0.00000001'
+ ID: a6aa69ba4cbe4467b669437d1efeaa0908c946cceca8cfb4cfbdfc334d5362df
+
+
+block_id: d350784b49d09d71e906899f47ce0962201e9938e9b9e2cf7489f95964d6b502
+height: 133
+transactions:
+- ID: 105fe7d2ccb474e93ed28b350c201be8cb6f84ef8eeef16aeff700b320521b0e
+ events:
+ - event: 'FlowToken.TokensWithdrawn: account=0x89c61aa64423504c, amount=0.00000001'
+ ID: f4642eaa93a5433eb8b7538a84f37084bc4331047bca9b257fd8004dbcabfc29
+ - event: 'FlowToken.TokensDeposited: account=0x754aed9de6197641, amount=0.00000001'
+ ID: 21fa643fef4cbc5cd6d09995f1872dfb3aa1a1477515ef218125793f492af09e
+
+
+block_id: 5d3490982b5f46e02bb2bfc447f4795b6563234a54d9b7e885c70efe8504017f
+height: 134
+transactions:
+- ID: 22006ab513a4670d0f2af36cc4e10725bc2d234d25f6e8de18fb2331ce1221ca
+ events:
+ - event: 'FlowToken.TokensWithdrawn: account=0x94b84d0c11a22404, amount=0.00000001'
+ ID: 50297e964be3e26fb3154793157b0593d1d1fa73808799249fefc86fec80476b
+ - event: 'FlowToken.TokensDeposited: account=0x754aed9de6197641, amount=0.00000001'
+ ID: cd3840b490c113f27ae0f8ca0fe7e4ccb90cd7adc5849e1e8d227a5b088650c4
+
+
+block_id: 886881a89ccb6d06095fa4098ec5954ebe783cfea6154beb054ee6c37f9c1c0a
+height: 135
+transactions:
+- ID: 4479cc1a0628463ef4669ac9a64e6056e382bba641bfe7e199e21301587936ac
+ events:
+ - event: 'FlowToken.TokensWithdrawn: account=0x668b91e2995c2eba, amount=0.00000001'
+ ID: 05f82e2a0e3ad5f033d7de2d8c6be8b0e34edf3b8b05b18b55bba197de237378
+ - event: 'FlowToken.TokensDeposited: account=0x754aed9de6197641, amount=0.00000001'
+ ID: 8465e7bc6ecadb2a691a3760bdc32b7901c8f8e9b77bf2686a6eeb7d64270dd3
+
+
+block_id: a27a2b007f9131d02b4bbfe655542e3d2946a662440d6ea8ec9778afcac17c99
+height: 137
+transactions:
+- ID: 5206a6302ada6a0b70f2d45400d27dcb55347fd993f6a76d60a73cd25fe40d1e
+ events:
+ - event: 'FlowToken.TokensWithdrawn: account=0x70dff4d1005824db, amount=0.00000001'
+ ID: 09fdc57d064c2d3a265fbb64db34011899e69d65dff625ddc3c4c86bce2866ed
+ - event: 'FlowToken.TokensDeposited: account=0x754aed9de6197641, amount=0.00000001'
+ ID: 9f8cdfb5cc5fcc22db5d7da41eedb356f11810a74ba84ec20c45732154453597
+
+
+block_id: 4fa55a014480c3390570e0683fd856e287593b8c2447e3b3b3232f041abc06ee
+height: 140
+transactions:
+- ID: 4c0b901a0c50b2334a7e87b863a801f344fe3b121f1fb19e97d78ddde2ef47e2
+ events:
+ - event: 'AccountCreated: 0x7bf5c648ccdd5af2'
+ ID: aa7396c0d50dcdc9320e90a6c40f11487ec402cd98a38ff799fa6131b3f74039
+ - event: flow.AccountKeyAdded
+ ID: 7b2a046316961af4bf7585b1dc12b70fbb62335f91c0872237e22762facc22cd
+ - event: 'FlowToken.TokensWithdrawn: account=0x8c5303eaa26202d6, amount=100.00000000'
+ ID: 0c5c276e60758c3987ff2106284f476a48c40aec2cdec9fa37360d72da5df215
+ - event: 'FlowToken.TokensDeposited: account=0x7bf5c648ccdd5af2, amount=100.00000000'
+ ID: d42965efc11b0aac322dbce0a8e15474551b7613b90b9988f972e31ef42653e3
+
+
+block_id: a40a2fb57039e03bd41d7d0d529d21e701c2b5e27e881b6f651389996258930c
+height: 141
+transactions:
+- ID: b817a19e5b34a70df35847e4f91d243ddfc66601f1fc69b3931ecc54d105b95b
+ events:
+ - event: 'FlowToken.TokensWithdrawn: account=0x877931736ee77cff, amount=0.00000001'
+ ID: 3743b61ae2848d3b4710118e7c4ab3a7e8d1b503e8f412a2c8036464a49a5b1d
+ - event: 'FlowToken.TokensDeposited: account=0x754aed9de6197641, amount=0.00000001'
+ ID: 0d388b0fc099a283d2d1971cf7188931a9f899baf9b57f21ad47d09423dddbb3
+
+
+block_id: db794d28c8003517961a47172f90a5c238ff58545ccdadfffccb84bb1767f1a8
+height: 142
+transactions:
+- ID: 69387f219848cc6d6186f44cce0ffb84a120d9a9dab1ec26a1fadc91abd848b6
+ events:
+ - event: 'FlowToken.TokensWithdrawn: account=0x754aed9de6197641, amount=0.00000001'
+ ID: ba5a1cc79544e0151a4541eba0a2c0fc151714c89f027d3af418bfa4168c18ee
+ - event: 'FlowToken.TokensDeposited: account=0x9f927f95dd275a2d, amount=0.00000001'
+ ID: 8b109510162b0157f2040b87529c1e1e4e2331c748456d5f060112a440cd311a
+
+
+block_id: 0dd40aa8905c73c83b85c001ecac897425fc86386cce16fa6d4d79159dc143a3
+height: 143
+transactions:
+- ID: b091ce3a882e6887f53477422c2efcb5ce2fe5de78093d31e52dc8868338b4d4
+ events:
+ - event: 'FlowToken.TokensWithdrawn: account=0x631e88ae7f1d7c20, amount=0.00000001'
+ ID: 9c32ee2fad9b446e3005d020af5c36d56fd63ac0412a48384ec59c7861f0a9f9
+ - event: 'FlowToken.TokensDeposited: account=0x6da1a37b55d95093, amount=0.00000001'
+ ID: 1dbbc3bab21f49cf09410fc90041ab2a3bff96e866629f4fdf8489d20e1fc6dc
+
+
+block_id: 1ec9096aae8eee7e1c0d52cfa0dc8a1d1b8650bcfb357c0cf690a3b5ebe84b4a
+height: 144
+transactions:
+- ID: 415fc16f95540ae5a87b6936ba749c002083d23ddae2b6e9dc1041ff0f7f00ea
+ events:
+ - event: 'FlowToken.TokensWithdrawn: account=0x9f927f95dd275a2d, amount=0.00000001'
+ ID: b0be4a8e1358153e4da1ae7cea5edd50691474d6a7317f66b7bc1655212f3104
+ - event: 'FlowToken.TokensDeposited: account=0x82ec283f88a62e65, amount=0.00000001'
+ ID: ba715a6b1b3e75d8db83bf31f9e6af6220096a35a33f6d3db7358df0baf26743
+
+
+block_id: 344a71b5f6c73ab49b728f809ceaf53789304f61a73394a170d533c577e31ed2
+height: 145
+transactions:
+- ID: 7d6cc25d229bb7fc531fca311eeb4b2d1b373a421f4132bc6cc17496d59a0349
+ events:
+ - event: 'FlowToken.TokensWithdrawn: account=0x82ec283f88a62e65, amount=0.00000001'
+ ID: 2d6dd3ca2d4b6c56f43f4442a174aee35402c80cc511d282a6d57679f5462e77
+ - event: 'FlowToken.TokensDeposited: account=0x6da1a37b55d95093, amount=0.00000001'
+ ID: 763ecb073634351883c83883cd94e0bcbc2be59054aacda130347c6e8cb1886b
+
+
+block_id: edf355798c49c6a5fd5c4ff917182f091ca8e02f4303e27c6d03077f5f3c485f
+height: 146
+transactions:
+- ID: 443c383bb8450169ef577aa476f018f839459d7fd11997b6af697bd7fbbbbb84
+ events:
+ - event: 'FlowToken.TokensWithdrawn: account=0x94b84d0c11a22404, amount=0.00000001'
+ ID: faf4233de02adc64b97c8f4b70237c281043d11fd8c961dc6cccaa9fb651a609
+ - event: 'FlowToken.TokensDeposited: account=0x89c61aa64423504c, amount=0.00000001'
+ ID: e484c0baf5ace58cfd432910983e49fe103f058e76ed3f4622878418ef97750b
+
+
+block_id: e4d020600cc4a475a1c23ab1017e9a09b2a74a6ab56577ef496c92f0ef3ef4b2
+height: 148
+transactions:
+- ID: e0602add78d489ec3eff4a15e29b7bbb77cbb65be91dbad473e84f0c4a1219bf
+ events:
+ - event: 'FlowToken.TokensWithdrawn: account=0x6da1a37b55d95093, amount=0.00000001'
+ ID: 75bae72f2446ef07782685d2db3a23c2c8058c73ea82fa14f2f1cc750410567d
+ - event: 'FlowToken.TokensDeposited: account=0x89c61aa64423504c, amount=0.00000001'
+ ID: 1123327aef9d86ab85043a16f5291febc1496c0700b17907b06b6b3bbf613085
+
+
+block_id: 277dbb3cd0ca876a57f17d5dd4450b2263a96344dafd6d2bcb39486116b68cf9
+height: 150
+transactions:
+- ID: 85bd640e89368208a08f5abbe0c5001235f43bee97d57b9aef11aebb9056592b
+ events:
+ - event: 'FlowToken.TokensWithdrawn: account=0x70dff4d1005824db, amount=0.00000001'
+ ID: 2f996ed77d821fa2289a5a5f5bb4b8fb78b2e384f000f8ebcf205fa51c519999
+ - event: 'FlowToken.TokensDeposited: account=0x668b91e2995c2eba, amount=0.00000001'
+ ID: 273504a00150e4a976d8e7ec49f341b3a5c7bece2e1463d183bb01e9aa945c53
+
+
+block_id: 0337cd8fbeff2167ed00d5414e98a5c6e1f2c8b2f75ecf467200da539e23ef09
+height: 151
+transactions:
+- ID: 63e4af3fbc8a76e5d1b7d3b6a59845b05750c5ded0e6c1ace26ed302f44a089f
+ events:
+ - event: 'FlowToken.TokensWithdrawn: account=0x89c61aa64423504c, amount=0.00000001'
+ ID: 996bc876bcca260c3b774309c64e1119ac8abe8da62d1eefa632ed6a70678f3d
+ - event: 'FlowToken.TokensDeposited: account=0x668b91e2995c2eba, amount=0.00000001'
+ ID: 37294c7b34cfa701db2939d49b69b6ce9a9ccadf5f03c81cbef47fe969077813
+
+
+block_id: 62aea07701614d56ef21b3f23c742b3b4df300a544e12f8d75c7cb845b3f1df0
+height: 152
+transactions:
+- ID: bce79e169bff6951b75db879d90d994df3c767bdd77a2c9c78fa0c5e4e26c74e
+ events:
+ - event: 'AccountCreated: 0x9672c1aa6286e0a8'
+ ID: 4602a304a0430eb0a535299055e317d40c0b654193799327078f4deceea01109
+ - event: flow.AccountKeyAdded
+ ID: 6f3acdc31bbc8c13524b9c682950c0cdb9724a7d8dcd63d79017b50ca64ce0f3
+ - event: 'FlowToken.TokensWithdrawn: account=0x8c5303eaa26202d6, amount=100.00000000'
+ ID: 1f30bebb19c7865fbcb85371a68a281fb6564b1ce9c72f220fa9a1b7847d819d
+ - event: 'FlowToken.TokensDeposited: account=0x9672c1aa6286e0a8, amount=100.00000000'
+ ID: eac4dcbf3bed502eb02ca806099ed67fe8b441fc56660c00ab89f7337e787855
+
+
+block_id: 8d03e1b71a5a7c37350664d2cb2583747f84d9ff0b083a1ae9a63da564cd3d66
+height: 154
+transactions:
+- ID: 1296b250275dd945970def0d9a981ca6e985080a4e4c27e8a603c096d026bc29
+ events:
+ - event: 'FlowToken.TokensWithdrawn: account=0x668b91e2995c2eba, amount=0.00000001'
+ ID: 140e66f1d3ec35d871258908814b3d94b70aea81dcc580af6d19fc6236008187
+ - event: 'FlowToken.TokensDeposited: account=0x7bf5c648ccdd5af2, amount=0.00000001'
+ ID: f02eef005a2aadee04fbdaae8db242b41090d05809bc683ee8f74b2a558ee8f5
+
+
+block_id: b3794133eaf9b90aa7c745b18fef0b6044bf8fc63143e9e2189f862470d6fa15
+height: 155
+transactions:
+- ID: 2aa47eb1307e6688300cb796e841c0f30db4b1000452c674344af8183f26dda5
+ events:
+ - event: 'FlowToken.TokensWithdrawn: account=0x7bf5c648ccdd5af2, amount=0.00000001'
+ ID: 2f3ec58a76611a38aa96a45410334b518a772a868c7c9aa3ca89b42ded0790d2
+ - event: 'FlowToken.TokensDeposited: account=0x877931736ee77cff, amount=0.00000001'
+ ID: e00a238c5212468b47c6670183fe57999c8d689d989ac71e738e008383ae25d5
+
+
+block_id: 53664a7bcb896db9416d12a2efd3c5090a02af16787e2583149f89b917b2f573
+height: 156
+transactions:
+- ID: 0fc5a90f31faa88bfa91f671e8dbef7b1a5e4c3648242a67e34188befd4d7743
+ events:
+ - event: 'FlowToken.TokensWithdrawn: account=0x754aed9de6197641, amount=0.00000001'
+ ID: 1ed1b4ad1252b2708403c8347f4b1a501e7b6d8075e2d5e7e463702b7afa5ec2
+ - event: 'FlowToken.TokensDeposited: account=0x877931736ee77cff, amount=0.00000001'
+ ID: eac225cc43b6ffdeb0f8de55507d0407c7cda847fa239a3051019dce8880d7ba
+
+
+block_id: 9859cb5355ce6549e52ba76aa3a93c8be40a573cdfda63619edb1a804534f43c
+height: 157
+transactions:
+- ID: a70179a3cdd38184a37e9122df5ba7294216cada831b95d8ccb310758218aff0
+ events:
+ - event: 'FlowToken.TokensWithdrawn: account=0x877931736ee77cff, amount=0.00000001'
+ ID: fb49c1b5815f2cfb82bbd7e1e4caf9c9a376b6bda38005dad5daacd66de8e0e7
+ - event: 'FlowToken.TokensDeposited: account=0x9f927f95dd275a2d, amount=0.00000001'
+ ID: 445f689f28d91f4371028d5922cfc18562b193f00034089aa77f34c6bae05f4e
+
+
+block_id: 62469348a94f75248ab9ca808036348269757ba3a5779792c7156606432c8074
+height: 159
+transactions:
+- ID: f9aafa6808ae7ffc63110d5e84ed5deca3a5f3603f3dcd1b27fb3d9a30760e55
+ events:
+ - event: 'FlowToken.TokensWithdrawn: account=0x631e88ae7f1d7c20, amount=0.00000001'
+ ID: f55733bb2f0beac99d4df9122b9e1c8bae54197250c14e76df4b2f3ff4d57d69
+ - event: 'FlowToken.TokensDeposited: account=0x82ec283f88a62e65, amount=0.00000001'
+ ID: eb88c015c937b80e7f61d1a185f480c8ab3045b8515220d0e40cb2bfbfab20bf
+
+
+block_id: eaba8fa79564c8bd5052e954ae7bf6e0dcac40f783ac60f3c7840f064bcdad38
+height: 160
+transactions:
+- ID: cd1f4858de60e5f1c726a600df3fb298e41a028c494f7449ffe2d82ad93eb291
+ events:
+ - event: 'FlowToken.TokensWithdrawn: account=0x94b84d0c11a22404, amount=0.00000001'
+ ID: a7ae7a13feff30c0fd82850ab028fd099484ea338fdadb009bdefc1ff5be2701
+ - event: 'FlowToken.TokensDeposited: account=0x9f927f95dd275a2d, amount=0.00000001'
+ ID: b13cfeb39383ef33956b6f37dd5157f3bc75aecc36391e7f49671873354cfcca
+
+
+block_id: 60edf45d755ed90ce4f07a9cb7465098eac8a8d128ff609419558ada91768e02
+height: 161
+transactions:
+- ID: 1d6196a2a570a3ed2c06930bd45019f149b26e6c4b7d5e08897596285f736628
+ events:
+ - event: 'FlowToken.TokensWithdrawn: account=0x6da1a37b55d95093, amount=0.00000001'
+ ID: b89d544445be892953a0ef34568bf891be5bcf835cb94c2e297ec4bc7cd4391b
+ - event: 'FlowToken.TokensDeposited: account=0x9f927f95dd275a2d, amount=0.00000001'
+ ID: ef05a96c544e88408cf79b33345174321774239208b4eecb3f13706abb134dc3
+
+
+block_id: 1992ebdb77c42f3f50b364a7b9c467d14e22a267d4ad7043c2bea0addc93156b
+height: 162
+transactions:
+- ID: 0a8df653a6d6ed73435abc5d0d389f93a387c3abe5b8b69e2221d401c35b4446
+ events:
+ - event: 'FlowToken.TokensWithdrawn: account=0x70dff4d1005824db, amount=0.00000001'
+ ID: 695538122977e325af2e7b00538c9246e9f142550f22b0649d7e7dd8c7e12f3c
+ - event: 'FlowToken.TokensDeposited: account=0x9f927f95dd275a2d, amount=0.00000001'
+ ID: 9353036129227d496c7880e172d8a0ba83ee4e63bd09804d1d451d365d14b9bb
+
+
+block_id: 99e4790796133481829c0ed643cb2187a8ab3abf23a1385de7a8f8644035c882
+height: 164
+transactions:
+- ID: d47c0a8ac36418f277bd580c43ecebee8d9ba5c6746d6c43733be49c703b6327
+ events:
+ - event: 'FlowToken.TokensWithdrawn: account=0x9f927f95dd275a2d, amount=0.00000001'
+ ID: 68904cbdd428889c465865c456c8cd03c90ede8fa57bc4da834a8aaebeeb28bb
+ - event: 'FlowToken.TokensDeposited: account=0x82ec283f88a62e65, amount=0.00000001'
+ ID: 59a2c5b369e47f95cd31c7b42be88d2c7faed9bb44cab8367b3310a62c1793d7
+
+
+block_id: ad5f39a9f8d95ba4bceef55a9ca753bb797dbe847a8e80ea784139ac28d9833c
+height: 165
+transactions:
+- ID: 23c486cfd54bca7138b519203322327bf46e43a780a237d1c5bb0a82f0a06c1d
+ events:
+ - event: 'AccountCreated: 0x72157877737ce077'
+ ID: e5066494dac0521ba1f42cc979f71412f8cd3fbd803b5bcdae3a9b7baf47b1e0
+ - event: flow.AccountKeyAdded
+ ID: f93be0445a858ddf9846818116350137c9f75166fd70b00b5044bfb1b3760b73
+ - event: 'FlowToken.TokensWithdrawn: account=0x8c5303eaa26202d6, amount=100.00000000'
+ ID: d3e50a104299316260a89d7340d4e914d88a002e7e0a0ba5a17e9018c8ca9325
+ - event: 'FlowToken.TokensDeposited: account=0x72157877737ce077, amount=100.00000000'
+ ID: 561193a787fe9b6220af92fdc6755c2d5a03848187608d8b34084a5c151cc291
+- ID: 3d6922d6c6fd161a76cec23b11067f22cac6409a49b28b905989db64f5cb05a5
+ events:
+ - event: 'FlowToken.TokensWithdrawn: account=0x89c61aa64423504c, amount=0.00000001'
+ ID: 335840041550db48d8c39b229db2f79c412df7016b30542f5685ebc8ed902f2b
+ - event: 'FlowToken.TokensDeposited: account=0x82ec283f88a62e65, amount=0.00000001'
+ ID: 595c713ac270311e0c204f57e26861b9e8fb111f6545a8c1fc8409e483298e03
+
+
+block_id: e6ba944b1863071484d4c16d663df15f52a020dd981df6676cfd40d37bb5c10b
+height: 166
+transactions:
+- ID: 08b3693774702ec8c70f1151181cda06087e2c7be451ade889c9960a3ae7ce78
+ events:
+ - event: 'FlowToken.TokensWithdrawn: account=0x668b91e2995c2eba, amount=0.00000001'
+ ID: 20b4934f9a0735bf0e2107e964b3c6aff9092f385e14db8b7378425d1256e293
+ - event: 'FlowToken.TokensDeposited: account=0x9672c1aa6286e0a8, amount=0.00000001'
+ ID: fa0149be199bc908f9598317ef9c6dc869a4ef3f597a2bce4f920d02f780a762
+
+
+block_id: f16693295a1dba3bcc4f4412a8d38238e25d9ea81ce2df9632605da2ebf2b6f1
+height: 167
+transactions:
+- ID: aed2ad1ee3bae52c3157ab07265042252068a58750e5503f26fc33b9643b59a2
+ events:
+ - event: 'FlowToken.TokensWithdrawn: account=0x82ec283f88a62e65, amount=0.00000001'
+ ID: 8d68d36de154fc9f0b9f46f209c52175cb9575308ae3cd0e9ee636adc07af55d
+ - event: 'FlowToken.TokensDeposited: account=0x9672c1aa6286e0a8, amount=0.00000001'
+ ID: c64e72a2ac1f63891752c6e06bb45792b9f6c116323f2d09e6320529486435c5
+
+
+block_id: 1bda5335ac521b72a9f0f75d116eaf8c372fa8cacda0c10b451d8e14b152d736
+height: 169
+transactions:
+- ID: 536ffbbf0ede1bfcaa0361ed1dc719f818c29a3f36ddb00c0a70cf516e8b2174
+ events:
+ - event: 'FlowToken.TokensWithdrawn: account=0x7bf5c648ccdd5af2, amount=0.00000001'
+ ID: 1c9c04f999fb6b39e05b569c5dd4ceac9ce835132c5cfeaa806e5d6d1a8b4a18
+ - event: 'FlowToken.TokensDeposited: account=0x9672c1aa6286e0a8, amount=0.00000001'
+ ID: f881e7544ad986f5e91508d6277447cf076f4b9911b2e728c25166e78ee5253a
+
+
+block_id: 49b3dd9c8790426384ec3dd1714dd927e76417f710261a9c18be86a13670aa41
+height: 170
+transactions:
+- ID: 8a8dac91d86514eb22b55a52d92cd28df9c01b60963ec51cd399f2df9c64c79f
+ events:
+ - event: 'FlowToken.TokensWithdrawn: account=0x754aed9de6197641, amount=0.00000001'
+ ID: a7afe88af2812d9661c2254d93fb405d94c6a5c04fdae593fc944d2831e0a106
+ - event: 'FlowToken.TokensDeposited: account=0x877931736ee77cff, amount=0.00000001'
+ ID: 1729cafc10e21c82e57c46829ba3c0f591ee31187d06f3f7c111439d68ac4132
+
+
+block_id: 212671f25759a36c11845b7946b9486ed7900c1c2e00c463243888f79bddb976
+height: 171
+transactions:
+- ID: 10c5a5971e2ed916aacf3a073e49406ed4190e12fef766fa68031994be9e60a0
+ events:
+ - event: 'FlowToken.TokensWithdrawn: account=0x9672c1aa6286e0a8, amount=0.00000001'
+ ID: 600d3ff290bbed7bff5d824563298fb3d81215006163175d748bbb8702163304
+ - event: 'FlowToken.TokensDeposited: account=0x877931736ee77cff, amount=0.00000001'
+ ID: a988792566165390dcda571b3ae2547db5c9140eb028b200e2c5a3d305263910
+
+
+block_id: abea1671ff37d197b933d9465ff2d31fb637c6cc7579f8a1902c52f59e482a93
+height: 172
+transactions:
+- ID: 12e377043d0ce6630e754c16f91eee6b5304e3a44f40767fb7b2c6d84f718a8f
+ events:
+ - event: 'FlowToken.TokensWithdrawn: account=0x877931736ee77cff, amount=0.00000001'
+ ID: f352f071a84f0f89ff57b327940574db9a587f8ae52eb4750aef28e368bd1a02
+ - event: 'FlowToken.TokensDeposited: account=0x631e88ae7f1d7c20, amount=0.00000001'
+ ID: 23056f03fe4e02537436b24ad0af3ddccef66a64ce9a7e4cc166517065042c18
+
+
+block_id: 6f4c1f12a6c4a8f8ed832e86e77d6f919029baad6b4c3a683e29de5be4cabd0f
+height: 173
+transactions:
+- ID: 3fb1e1bbbdc7f0b04bd687c990cc102e6e769d89bd7c77a684bcb10ad7e51ba5
+ events:
+ - event: 'FlowToken.TokensWithdrawn: account=0x631e88ae7f1d7c20, amount=0.00000001'
+ ID: f8f2a023a25494ede5c35608a88215416dd4899e0d919a749d13f8a93e204fd5
+ - event: 'FlowToken.TokensDeposited: account=0x94b84d0c11a22404, amount=0.00000001'
+ ID: 4f00f6ea94d97fd4f44fd9acc1e9668e8c586fc7c2dc7e69e6e2714bfbf8f1c1
+
+
+block_id: 54d0cbaa7938d39ef29d525044d9d6e1b0b57f53ac848328a65ee5da6aadbe99
+height: 174
+transactions:
+- ID: a650a176618c68aca4640c300716601abd09b191454ae5b5977c0eddfe32356b
+ events:
+ - event: 'FlowToken.TokensWithdrawn: account=0x94b84d0c11a22404, amount=0.00000001'
+ ID: 2855ad008388c84e322e74f5bd8958d6ee97e4fd07c818f9825c20780cc8614c
+ - event: 'FlowToken.TokensDeposited: account=0x6da1a37b55d95093, amount=0.00000001'
+ ID: 709a4243128d10da66aec8a5074553472d518b4145d37180ccd6084db60335ae
+
+
+block_id: 8a3a4329b30f1379ac5cc1553105633ed67a10f2a04b818dd87e585b903c1a18
+height: 176
+transactions:
+- ID: 899bfc3a1e6ff3f9ad58e74a60a45fdadf300501abf063e631930ad1b0b67148
+ events:
+ - event: 'FlowToken.TokensWithdrawn: account=0x6da1a37b55d95093, amount=0.00000001'
+ ID: 08c4a35dd23e9962e3599b8696406417ca6a1ad211bb452b53e22dfbe0443a7d
+ - event: 'FlowToken.TokensDeposited: account=0x70dff4d1005824db, amount=0.00000001'
+ ID: 3533e59480f20fa4a3cc47b32a00dd5d6fdea026013f97d8ffbb44af4c44a55e
+
+
+block_id: cc9b4d7ba6aa2a90984d272547ec461d6bc8c00c919302f9f7c95e57750fcf8d
+height: 178
+transactions:
+- ID: 29f6ec05beafb005fc72d9155701727baf21e4bf6b8a32d3f105566632e581ac
+ events:
+ - event: 'FlowToken.TokensWithdrawn: account=0x70dff4d1005824db, amount=0.00000001'
+ ID: 7ab5abdd73bc28cce73c8c823283f95d4d3ff8f0b0ab31b677fdf5f84ba4df55
+ - event: 'FlowToken.TokensDeposited: account=0x9f927f95dd275a2d, amount=0.00000001'
+ ID: c24f8258cd2f7dfe8870815d01ee0c7d79a55abf73beaf9bb17acf83f8fa6117
+- ID: 6af65788d0adeed92459394daea245fe479c7fdefba7d9bb944252c68cadda18
+ events:
+ - event: 'AccountCreated: 0x64411d44ea78ea16'
+ ID: bd3a22683e8ce86dad58215ded290bc915b8a050db3f27c79c1f1e955b541ad4
+ - event: flow.AccountKeyAdded
+ ID: 48514b3b52166cbe5aeebec1d52ae50a4fbda3eda18579e7a636c5dc57fe59d0
+ - event: 'FlowToken.TokensWithdrawn: account=0x8c5303eaa26202d6, amount=100.00000000'
+ ID: 9e0c9a871f0d8cdd6488e76927b0b799925e4f6e3c7c39479145ae18e2e24340
+ - event: 'FlowToken.TokensDeposited: account=0x64411d44ea78ea16, amount=100.00000000'
+ ID: 321af8584b5d87afa4683f654a98090929f1e2bb84bc1b5d1d6786098742099b
+
+
+block_id: 6b344216c981f47439cb3d4b4a785d4d667d76d2fbb27a7732b262218cf21572
+height: 179
+transactions:
+- ID: 3d20d9d66bc0466825ffded0953fa5a528766b58f043a0f95cbf5cdb0d5ea8f9
+ events:
+ - event: 'FlowToken.TokensWithdrawn: account=0x72157877737ce077, amount=0.00000001'
+ ID: f731550a13f5af4d77cf737616d800346d73067a88b6888bd0ec778f41006aaa
+ - event: 'FlowToken.TokensDeposited: account=0x89c61aa64423504c, amount=0.00000001'
+ ID: 05b7302de0d1facda1a16776627f1f5a628295dd2ce4488858ae6a338cdd15ae
+
+
+block_id: 9e33a657088fc6f162c236ee1d8fc4782c9fc1b65baf4a400624f766369caad3
+height: 180
+transactions:
+- ID: 17d8067f671c76184dada2634807cf7e94e1f1795daa3f47edc32e9da0e2928a
+ events:
+ - event: 'FlowToken.TokensWithdrawn: account=0x9f927f95dd275a2d, amount=0.00000001'
+ ID: 80c5f800502328c0d09f7209121e9e8bc6ac60460622354cf40c11803648e928
+ - event: 'FlowToken.TokensDeposited: account=0x668b91e2995c2eba, amount=0.00000001'
+ ID: f2ab38176cb913623b52c81c04da637b1bc5d360e1923d81c8aae516e0b94cf2
+
+
+block_id: 0b11ddfc1d324ee830f27648166d1e52c5868096f43f840f7bd39a0be7346a11
+height: 181
+transactions:
+- ID: 780bafaf4721ca4270986ea51e659951a8912c2eb99fb1bfedeb753b023cd4d9
+ events:
+ - event: 'FlowToken.TokensWithdrawn: account=0x668b91e2995c2eba, amount=0.00000001'
+ ID: c8a754050ba241d4f361316ee3b6f95c3ef376b16ad2273dbc4e92e75c3f490d
+ - event: 'FlowToken.TokensDeposited: account=0x89c61aa64423504c, amount=0.00000001'
+ ID: fa847a3019da089a2d1aa0091c1fd6622c2a5f0d4f756b57d38935ea960270d9
+
+```
+
+
+
+## Balances
+
+### Notable balances referenced in tests
+
+| Account ID | Block Height | Balance |
+|------------------|--------------|-------------|
+| 754aed9de6197641 | 13 | 10000100000 |
+| 754aed9de6197641 | 50 | 10000099999 |
+| 754aed9de6197641 | 425 | 10000100002 |