diff --git a/backend/Dockerfile b/backend/Dockerfile index fca36d4..1a37339 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -16,4 +16,4 @@ RUN go build -ldflags="-s -w" -o /app/blockd cmd/main.go EXPOSE 8080 -CMD ["/app/blockd", "-log-level=debug","-log-local=false","-log-add-source=true","-rest-address=0.0.0.0:8080","-db-host=blockd-db:5432","-db-database=blockd","-db-user=blockd","-db-secret=blockd","-db-enable-tls=false"] +CMD ["/app/blockd", "-log-level=info","-log-local=false","-log-add-source=true","-rest-address=0.0.0.0:8080","-db-host=blockd-db:5432","-db-database=blockd","-db-user=blockd","-db-secret=blockd","-db-enable-tls=false", "-cache-host=blockd-cache:6379"] diff --git a/backend/cmd/main.go b/backend/cmd/main.go index 03bbff7..430a647 100644 --- a/backend/cmd/main.go +++ b/backend/cmd/main.go @@ -2,7 +2,6 @@ package main import ( "context" - "fmt" "os" "os/signal" "syscall" @@ -117,8 +116,6 @@ func main() { }, } - fmt.Println(config) - service, cleanup, err := factory.ProvideService(config) if err != nil { panic(err) diff --git a/backend/docker-compose.yaml b/backend/docker-compose.yaml index 2265184..0113591 100644 --- a/backend/docker-compose.yaml +++ b/backend/docker-compose.yaml @@ -17,15 +17,13 @@ services: context: . dockerfile: ./Dockerfile ports: - - 8080:8080 + - 8081:8080 networks: - blockd-net - syslog depends_on: blockd-db: condition: service_healthy - syslog: - condition: service_started profiles: [blockd] blockd-db: @@ -61,52 +59,53 @@ services: - 6379:6379 profiles: [blockd, database, noback] - # prometheus: - # image: prom/prometheus - # container_name: prometheus - # command: - # - '--config.file=/etc/prometheus/prometheus.yml' - # ports: - # - 9091:9090 - # restart: unless-stopped - # networks: - # - blockd-net - # volumes: - # - ./prometheus:/etc/prometheus - # - prometheus_data:/prometheus - # profiles: [blockd, metrics, noback] + prometheus: + image: prom/prometheus + container_name: prometheus + command: + - '--config.file=/etc/prometheus/prometheus.yml' + ports: + - 9091:9090 + restart: unless-stopped + networks: + - blockd-net + volumes: + - ./prometheus:/etc/prometheus + - prometheus_data:/prometheus + profiles: [metrics] - # grafana: - # image: grafana/grafana - # container_name: grafana - # ports: - # - 3112:3000 - # restart: unless-stopped - # networks: - # - blockd-net - # environment: - # - GF_SECURITY_ADMIN_USER=admin - # - GF_SECURITY_ADMIN_PASSWORD=grafana - # volumes: - # - ./grafana:/etc/grafana/provisioning/datasources - # profiles: [blockd, metrics, noback] + grafana: + image: grafana/grafana + container_name: grafana + ports: + - 3112:3000 + restart: unless-stopped + networks: + - blockd-net + environment: + - GF_SECURITY_ADMIN_USER=admin + - GF_SECURITY_ADMIN_PASSWORD=grafana + volumes: + - ./grafana:/etc/grafana/provisioning/datasources + profiles: [metrics] - # syslog: - # image: linuxserver/syslog-ng:3.36.1 - # container_name: syslog-ng - # environment: - # - PUID=0 - # - PGID=0 - # - TZ=UTC - # volumes: - # - /srv/syslog/config:/config - # - /srv/syslog/logs:/var/log - # ports: - # - 514:5514/udp - # - 601:6601/tcp - # - 6514:6514/tcp - # restart: unless-stopped - # networks: - # - syslog - # logging: - # driver: "json-file" \ No newline at end of file + syslog: + image: linuxserver/syslog-ng:3.36.1 + container_name: syslog-ng + environment: + - PUID=0 + - PGID=0 + - TZ=UTC + volumes: + - /srv/syslog/config:/config + - /srv/syslog/logs:/var/log + ports: + - 514:5514/udp + - 601:6601/tcp + - 6514:6514/tcp + restart: unless-stopped + networks: + - syslog + logging: + driver: "json-file" + profiles: [metrics] diff --git a/backend/internal/factory/interface.go b/backend/internal/factory/interface.go index 6f08493..0937835 100644 --- a/backend/internal/factory/interface.go +++ b/backend/internal/factory/interface.go @@ -16,6 +16,7 @@ import ( "github.com/emochka2007/block-accounting/internal/usecase/interactors/organizations" "github.com/emochka2007/block-accounting/internal/usecase/interactors/transactions" "github.com/emochka2007/block-accounting/internal/usecase/interactors/users" + "github.com/emochka2007/block-accounting/internal/usecase/repository/auth" ) var interfaceSet wire.ProviderSet = wire.NewSet( @@ -67,12 +68,14 @@ func provideAuthController( usersInteractor users.UsersInteractor, authPresenter presenters.AuthPresenter, jwtInteractor jwt.JWTInteractor, + repo auth.Repository, ) controllers.AuthController { return controllers.NewAuthController( log.WithGroup("auth-controller"), authPresenter, usersInteractor, jwtInteractor, + repo, ) } diff --git a/backend/internal/factory/wire_gen.go b/backend/internal/factory/wire_gen.go index 82078b8..e66f3d1 100644 --- a/backend/internal/factory/wire_gen.go +++ b/backend/internal/factory/wire_gen.go @@ -27,7 +27,7 @@ func ProvideService(c config.Config) (service.Service, func(), error) { authRepository := provideAuthRepository(db) jwtInteractor := provideJWTInteractor(c, usersInteractor, authRepository) authPresenter := provideAuthPresenter(jwtInteractor) - authController := provideAuthController(logger, usersInteractor, authPresenter, jwtInteractor) + authController := provideAuthController(logger, usersInteractor, authPresenter, jwtInteractor, authRepository) organizationsRepository := provideOrganizationsRepository(db, usersRepository) client, cleanup2 := provideRedisConnection(c) cache := provideRedisCache(client, logger) diff --git a/backend/internal/interface/rest/controllers/auth.go b/backend/internal/interface/rest/controllers/auth.go index eeb626c..f6ad795 100644 --- a/backend/internal/interface/rest/controllers/auth.go +++ b/backend/internal/interface/rest/controllers/auth.go @@ -2,18 +2,23 @@ package controllers import ( "context" + "crypto/sha256" + "encoding/base64" "errors" "fmt" "log/slog" "net/http" + "strings" "time" "github.com/emochka2007/block-accounting/internal/interface/rest/domain" "github.com/emochka2007/block-accounting/internal/interface/rest/presenters" "github.com/emochka2007/block-accounting/internal/pkg/bip39" + "github.com/emochka2007/block-accounting/internal/pkg/ctxmeta" "github.com/emochka2007/block-accounting/internal/pkg/hdwallet" "github.com/emochka2007/block-accounting/internal/usecase/interactors/jwt" "github.com/emochka2007/block-accounting/internal/usecase/interactors/users" + "github.com/emochka2007/block-accounting/internal/usecase/repository/auth" ) var ( @@ -34,6 +39,7 @@ type authController struct { presenter presenters.AuthPresenter usersInteractor users.UsersInteractor jwtInteractor jwt.JWTInteractor + repo auth.Repository } func NewAuthController( @@ -41,12 +47,14 @@ func NewAuthController( presenter presenters.AuthPresenter, usersInteractor users.UsersInteractor, jwtInteractor jwt.JWTInteractor, + repo auth.Repository, ) AuthController { return &authController{ log: log, presenter: presenter, usersInteractor: usersInteractor, jwtInteractor: jwtInteractor, + repo: repo, } } @@ -140,9 +148,64 @@ func (c *authController) Refresh(w http.ResponseWriter, req *http.Request) ([]by // const mnemonicEntropyBitSize int = 256 -func (c *authController) Invite(w http.ResponseWriter, req *http.Request) ([]byte, error) { +func (c *authController) Invite(w http.ResponseWriter, r *http.Request) ([]byte, error) { + request, err := presenters.CreateRequest[domain.NewInviteLinkRequest](r) + if err != nil { + return nil, fmt.Errorf("error create refresh request. %w", err) + } - return nil, nil + organizationID, err := ctxmeta.OrganizationId(r.Context()) + if err != nil { + return nil, fmt.Errorf("error fetch organization id from context. %w", err) + } + + user, err := ctxmeta.User(r.Context()) + if err != nil { + return nil, fmt.Errorf("error fetch user from context. %w", err) + } + + c.log.Debug( + "invite request", + slog.Int("exp at", request.ExpirationDate), + slog.String("org id", organizationID.String()), + slog.String("inviter id", user.Id().String()), + ) + + ctx, cancel := context.WithTimeout(r.Context(), 3*time.Second) + defer cancel() + + linkHash := sha256.New() + + linkHash.Write([]byte( + user.Id().String() + organizationID.String() + time.Now().String(), + )) + + linkHashString := strings.ReplaceAll( + strings.ReplaceAll( + strings.ReplaceAll( + base64.StdEncoding.EncodeToString(linkHash.Sum(nil)), + "/", "%", + ), + "?", "@", + ), + "&", "#", + ) + + c.log.Debug( + "", + slog.String("link", linkHashString), + ) + + if err := c.repo.AddInvite(ctx, auth.AddInviteParams{ + LinkHash: linkHashString, + CreatedBy: *user, + CreatedAt: time.Now(), + ExpiredAt: time.UnixMilli(int64(request.ExpirationDate)), + }); err != nil { + return nil, fmt.Errorf("error add new invite link. %w", err) + } + + return c.presenter.ResponseNewInvite(ctx, organizationID, linkHashString) } func (c *authController) JoinWithInvite(w http.ResponseWriter, req *http.Request) ([]byte, error) { diff --git a/backend/internal/interface/rest/domain/dto.go b/backend/internal/interface/rest/domain/dto.go index ee98247..116197b 100644 --- a/backend/internal/interface/rest/domain/dto.go +++ b/backend/internal/interface/rest/domain/dto.go @@ -112,7 +112,11 @@ type NewMultisigRequest struct { Owners []struct { PublicKey string `json:"public_key"` } `json:"owners"` - Confirmations int `json:confirmations` + Confirmations int `json:"confirmations"` +} + +type NewInviteLinkRequest struct { + ExpirationDate int `json:"expiration_date"` } func BuildRequest[T any](data []byte) (*T, error) { diff --git a/backend/internal/interface/rest/presenters/auth.go b/backend/internal/interface/rest/presenters/auth.go index 36f3e25..53ab719 100644 --- a/backend/internal/interface/rest/presenters/auth.go +++ b/backend/internal/interface/rest/presenters/auth.go @@ -1,6 +1,7 @@ package presenters import ( + "context" "encoding/json" "fmt" "time" @@ -8,12 +9,18 @@ import ( "github.com/emochka2007/block-accounting/internal/interface/rest/domain" "github.com/emochka2007/block-accounting/internal/pkg/models" "github.com/emochka2007/block-accounting/internal/usecase/interactors/jwt" + "github.com/google/uuid" ) type AuthPresenter interface { ResponseJoin(user *models.User) ([]byte, error) ResponseLogin(user *models.User) ([]byte, error) ResponseRefresh(tokens jwt.AccessToken) ([]byte, error) + ResponseNewInvite( + ctx context.Context, + organizationID uuid.UUID, + link string, + ) ([]byte, error) } type authPresenter struct { @@ -79,3 +86,18 @@ func (p *authPresenter) ResponseRefresh(tokens jwt.AccessToken) ([]byte, error) return out, nil } + +func (p *authPresenter) ResponseNewInvite( + ctx context.Context, + organizationID uuid.UUID, + link string, +) ([]byte, error) { + out, err := json.Marshal(map[string]string{ + "link": "/" + organizationID.String() + "/invite/" + link, + }) + if err != nil { + return nil, fmt.Errorf("error marshal refresh response. %w", err) + } + + return out, nil +} diff --git a/backend/internal/pkg/models/models.go b/backend/internal/pkg/models/models.go index 02ce654..51d9db9 100644 --- a/backend/internal/pkg/models/models.go +++ b/backend/internal/pkg/models/models.go @@ -12,6 +12,8 @@ type Multisig struct { OrganizationID uuid.UUID Owners []OrganizationParticipant ConfirmationsRequired int + CreatedAt time.Time + UpdatedAt time.Time } type MultisigConfirmation struct { diff --git a/backend/internal/usecase/repository/auth/errors.go b/backend/internal/usecase/repository/auth/errors.go new file mode 100644 index 0000000..e05067b --- /dev/null +++ b/backend/internal/usecase/repository/auth/errors.go @@ -0,0 +1,7 @@ +package auth + +import "errors" + +var ( + ErrorInviteLinkExpired = errors.New("invite link expired") +) diff --git a/backend/internal/usecase/repository/auth/repository.go b/backend/internal/usecase/repository/auth/repository.go index c3aa9c0..62a977c 100644 --- a/backend/internal/usecase/repository/auth/repository.go +++ b/backend/internal/usecase/repository/auth/repository.go @@ -8,6 +8,7 @@ import ( "time" sq "github.com/Masterminds/squirrel" + "github.com/emochka2007/block-accounting/internal/pkg/models" sqltools "github.com/emochka2007/block-accounting/internal/pkg/sqlutils" "github.com/google/uuid" ) @@ -60,6 +61,9 @@ type Repository interface { AddToken(ctx context.Context, params AddTokenParams) error GetTokens(ctx context.Context, params GetTokenParams) (*AccessToken, error) RefreshToken(ctx context.Context, params RefreshTokenParams) error + + AddInvite(ctx context.Context, params AddInviteParams) error + MarkAsUsedLink(ctx context.Context, linkHash string, usedAt time.Time) error } type repositorySQL struct { @@ -180,6 +184,70 @@ func (r *repositorySQL) GetTokens(ctx context.Context, params GetTokenParams) (* return token, nil } +type AddInviteParams struct { + LinkHash string + CreatedBy models.User + CreatedAt time.Time + ExpiredAt time.Time +} + +func (r *repositorySQL) AddInvite( + ctx context.Context, + params AddInviteParams, +) error { + return sqltools.Transaction(ctx, r.db, func(ctx context.Context) error { + query := sq.Insert("invites").Columns( + "link_hash", + "created_by", + "created_at", + "expired_at", + ).Values( + params.LinkHash, + params.CreatedBy.Id(), + params.CreatedAt, + params.ExpiredAt, + ).PlaceholderFormat(sq.Dollar) + + if _, err := query.RunWith(r.Conn(ctx)).ExecContext(ctx); err != nil { + return fmt.Errorf("error add invite link. %w", err) + } + + return nil + }) +} + +func (r *repositorySQL) MarkAsUsedLink( + ctx context.Context, + linkHash string, + usedAt time.Time, +) error { + return sqltools.Transaction(ctx, r.db, func(ctx context.Context) error { + query := sq.Select("expired_at").From("invites").Where(sq.Eq{ + "link_hash": linkHash, + }).Limit(1).PlaceholderFormat(sq.Dollar) + + var expAt time.Time + + if err := query.RunWith(r.Conn(ctx)).QueryRowContext(ctx).Scan(&expAt); err != nil { + return fmt.Errorf("error fetch expiration date from database. %w", err) + } + + if expAt.After(time.Now()) { + return ErrorInviteLinkExpired + } + + updateQuery := sq.Update("invites").SetMap(sq.Eq{ + "used_at": usedAt, + }) + + if _, err := updateQuery.RunWith(r.Conn(ctx)).ExecContext(ctx); err != nil { + return fmt.Errorf("error add invite link. %w", err) + } + + return nil + }) +} + func NewRepository(db *sql.DB) Repository { return &repositorySQL{ db: db, diff --git a/backend/internal/usecase/repository/db.go b/backend/internal/usecase/repository/db.go index b56b393..b782d87 100644 --- a/backend/internal/usecase/repository/db.go +++ b/backend/internal/usecase/repository/db.go @@ -20,8 +20,6 @@ func ProvideDatabaseConnection(c config.Config) (*sql.DB, func(), error) { c.DB.User, c.DB.Secret, c.DB.Host, c.DB.Database, sslmode, ) - fmt.Println(connStr) - db, err := sql.Open("postgres", connStr) if err != nil { return nil, func() {}, fmt.Errorf("error connecting to database: %w", err) diff --git a/backend/internal/usecase/repository/organizations/repository.go b/backend/internal/usecase/repository/organizations/repository.go index 639f51b..781502c 100644 --- a/backend/internal/usecase/repository/organizations/repository.go +++ b/backend/internal/usecase/repository/organizations/repository.go @@ -475,8 +475,6 @@ func (r *repositorySQL) Participants( }) } - fmt.Println(query.ToSql()) - rows, err := query.RunWith(r.Conn(ctx)).QueryContext(ctx) if err != nil { return fmt.Errorf("error fetch employees from database. %w", err) @@ -740,8 +738,6 @@ func (r *repositorySQL) fetchEmployees( }) } - fmt.Println(query.ToSql()) - rows, err := query.RunWith(r.Conn(ctx)).QueryContext(ctx) if err != nil { return fmt.Errorf("error fetch employees from database. %w", err) @@ -808,7 +804,7 @@ func (r *repositorySQL) AddEmployee(ctx context.Context, employee models.Employe employee.WalletAddress, employee.CreatedAt, employee.UpdatedAt, - ) + ).PlaceholderFormat(sq.Dollar) if _, err := query.RunWith(r.Conn(ctx)).ExecContext(ctx); err != nil { return fmt.Errorf("error add employee. %w", err) diff --git a/backend/internal/usecase/repository/transactions/repository.go b/backend/internal/usecase/repository/transactions/repository.go index 45a4ba1..8129fcd 100644 --- a/backend/internal/usecase/repository/transactions/repository.go +++ b/backend/internal/usecase/repository/transactions/repository.go @@ -50,6 +50,8 @@ type Repository interface { ConfirmTransaction(ctx context.Context, params ConfirmTransactionParams) error CancelTransaction(ctx context.Context, params CancelTransactionParams) error + + AddMultisig(ctx context.Context, multisig models.Multisig) error } type repositorySQL struct { @@ -89,8 +91,6 @@ func (r *repositorySQL) GetTransactions( } }() - fmt.Println(query.ToSql()) - for rows.Next() { var ( id uuid.UUID @@ -400,3 +400,48 @@ func buildGetTransactionsQuery(params GetTransactionsParams) sq.SelectBuilder { return query } + +func (r *repositorySQL) AddMultisig( + ctx context.Context, + multisig models.Multisig, +) error { + return sqltools.Transaction(ctx, r.db, func(ctx context.Context) error { + query := sq.Insert("multisigs").Columns( + "id", + "organization_id", + "title", + "created_at", + "updated_at", + ).Values( + multisig.ID, + multisig.OrganizationID, + multisig.Title, + multisig.CreatedAt, + multisig.UpdatedAt, + ).PlaceholderFormat(sq.Dollar) + + if _, err := query.RunWith(r.Conn(ctx)).ExecContext(ctx); err != nil { + return fmt.Errorf("error insert multisig data. %w", err) + } + + for _, owner := range multisig.Owners { + addOwnerQuery := sq.Insert("multisig_owners").Columns( + "multisig_id", + "owner_id", + "created_at", + "updated_at", + ).Values( + multisig.ID, + owner.Id(), + multisig.CreatedAt, + multisig.UpdatedAt, + ) + + if _, err := addOwnerQuery.RunWith(r.Conn(ctx)).ExecContext(ctx); err != nil { + return fmt.Errorf("error insert multisig owner data. %w", err) + } + } + + return nil + }) +} diff --git a/backend/internal/usecase/repository/users/repository.go b/backend/internal/usecase/repository/users/repository.go index 4ef1648..5f73373 100644 --- a/backend/internal/usecase/repository/users/repository.go +++ b/backend/internal/usecase/repository/users/repository.go @@ -72,8 +72,6 @@ func (r *repositorySQL) Get(ctx context.Context, params GetParams) ([]*models.Us query = query.Where("u.seed = ?", params.Seed) } - fmt.Println(query.ToSql()) - rows, err := query.RunWith(r.Conn(ctx)).QueryContext(ctx) if err != nil { return fmt.Errorf("error fetch data from database. %w", err) diff --git a/backend/migrations/blockd.sql b/backend/migrations/blockd.sql index 1bac109..a6794fb 100644 --- a/backend/migrations/blockd.sql +++ b/backend/migrations/blockd.sql @@ -149,16 +149,18 @@ create table contracts ( updated_at timestamp default current_timestamp ); -create intex if not exists idx_contracts_organization_id_multisig +create index if not exists idx_contracts_organization_id_multisig on contracts (organization_id, multisig); -create intex if not exists idx_contracts_organization_id_created_by +create index if not exists idx_contracts_organization_id_created_by on contracts (organization_id, created_by); create table multisigs ( id uuid primary key, organization_id uuid not null references organizations(id), - title varchar(350) default 'New Multi-Sig' + title varchar(350) default 'New Multi-Sig', + created_at timestamp default current_timestamp, + updated_at timestamp default current_timestamp ); create table multisig_owners ( @@ -169,10 +171,10 @@ create table multisig_owners ( primary key (multisig_id, owner_id) ); -create intex if not exists idx_multisig_owners_multisig_id +create index if not exists idx_multisig_owners_multisig_id on multisig_owners (multisig_id); -create intex if not exists idx_multisig_owners_owner_id +create index if not exists idx_multisig_owners_owner_id on multisig_owners (owner_id); create table multisig_confirmations ( @@ -182,14 +184,14 @@ create table multisig_confirmations ( primary key (multisig_id, owner_id) ); -create intex if not exists idx_multisig_confirmations_owners_multisig_id +create index if not exists idx_multisig_confirmations_owners_multisig_id on multisig_confirmations (multisig_id); -create intex if not exists idx_multisig_confirmations_owners_owner_id +create index if not exists idx_multisig_confirmations_owners_owner_id on multisig_confirmations (owner_id); create table invites ( - link_hash bytea primary key, + link_hash varchar(64) primary key, created_by uuid not null references users(id), created_at timestamp default current_timestamp, expired_at timestamp default null, diff --git a/backend/test.txt b/backend/test.txt deleted file mode 100644 index b04b500..0000000 --- a/backend/test.txt +++ /dev/null @@ -1,554 +0,0 @@ - - -Австралийский доллар -Australian Dollar -1 -R01010 -36 -AUD - - -Австрийский шиллинг -Austrian Shilling -1000 -R01015 -40 -ATS - - -Азербайджанский манат -Azerbaijan Manat -1 -R01020 -944 -AZN - - -Фунт стерлингов Соединенного королевства -British Pound Sterling -1 -R01035 -826 -GBP - - -Ангольская новая кванза -Angolan new Kwanza -100000 -R01040 -24 -AON - - -Армянский драм -Armenia Dram -100 -R01060 -51 -AMD - - -Белорусский рубль -Belarussian Ruble -1 -R01090 -933 -BYN - - -Бельгийский франк -Belgium Franc -1000 -R01095 -56 -BEF - - -Болгарский лев -Bulgarian lev -1 -R01100 -975 -BGN - - -Бразильский реал -Brazil Real -1 -R01115 -986 -BRL - - -Венгерский форинт -Hungarian Forint -100 -R01135 -348 -HUF - - -Вьетнамский донг -Vietnam Dong -10000 -R01150 -704 -VND - - -Гонконгский доллар -Hong Kong Dollar -1 -R01200 -344 -HKD - - -Греческая драхма -Greek Drachma -10000 -R01205 -300 -GRD - - -Грузинский лари -Georgia Lari -1 -R01210 -981 -GEL - - -Датская крона -Danish Krone -1 -R01215 -208 -DKK - - -Дирхам ОАЭ -UAE Dirham -1 -R01230 -784 -AED - - -Доллар США -US Dollar -1 -R01235 -840 -USD - - -Евро -Euro -1 -R01239 -978 -EUR - - -Египетский фунт -Egyptian Pound -10 -R01240 -818 -EGP - - -Индийская рупия -Indian Rupee -10 -R01270 -356 -INR - - -Индонезийская рупия -Indonesian Rupiah -10000 -R01280 -360 -IDR - - -Ирландский фунт -Irish Pound -100 -R01305 -372 -IEP - - -Исландская крона -Iceland Krona -10000 -R01310 -352 -ISK - - -Испанская песета -Spanish Peseta -10000 -R01315 -724 -ESP - - -Итальянская лира -Italian Lira -100000 -R01325 -380 -ITL - - -Казахстанский тенге -Kazakhstan Tenge -100 -R01335 -398 -KZT - - -Канадский доллар -Canadian Dollar -1 -R01350 -124 -CAD - - -Катарский риал -Qatari Riyal -1 -R01355 -634 -QAR - - -Киргизский сом -Kyrgyzstan Som -10 -R01370 -417 -KGS - - -Китайский юань -China Yuan -1 -R01375 -156 -CNY - - -Кувейтский динар -Kuwaiti Dinar -10 -R01390 -414 -KWD - - -Латвийский лат -Latvian Lat -1 -R01405 -428 -LVL - - -Ливанский фунт -Lebanese Pound -100000 -R01420 -422 -LBP - - -Литовский лит -Lithuanian Lita -1 -R01435 -440 -LTL - - -Литовский талон -Lithuanian talon -1 -R01435 - - - - -Молдавский лей -Moldova Lei -10 -R01500 -498 -MDL - - -Немецкая марка -Deutsche Mark -1 -R01510 -276 -DEM - - -Немецкая марка -Deutsche Mark -100 -R01510 -280 -DEM - - -Нидерландский гульден -Netherlands Gulden -100 -R01523 -528 -NLG - - -Новозеландский доллар -New Zealand Dollar -1 -R01530 -554 -NZD - - -Норвежская крона -Norwegian Krone -10 -R01535 -578 -NOK - - -Польский злотый -Polish Zloty -1 -R01565 -985 -PLN - - -Португальский эскудо -Portuguese Escudo -10000 -R01570 -620 -PTE - - -Румынский лей -Romanian Leu -10000 -R01585 -642 -ROL - - -Румынский лей -Romanian Leu -1 -R01585 -946 -RON - - -СДР (специальные права заимствования) -SDR -1 -R01589 -960 -XDR - - -Сингапурский доллар -Singapore Dollar -1 -R01625 -702 -SGD - - -Суринамский доллар -Surinam Dollar -1 -R01665 -968 -SRD - - -Таджикский сомони -Tajikistan Ruble -10 -R01670 -972 -TJS - - -Таиландский бат -Thai Baht -10 -R01675 -764 -THB - - -Турецкая лира -Turkish Lira -10 -R01700 -949 -TRY - - -Туркменский манат -Turkmenistan Manat -10000 -R01710 -795 -TMM - - -Новый туркменский манат -New Turkmenistan Manat -1 -R01710 -934 -TMT - - -Узбекский сум -Uzbekistan Sum -10000 -R01717 -860 -UZS - - -Украинская гривна -Ukrainian Hryvnia -10 -R01720 -980 -UAH - - -Украинский карбованец -Ukrainian Hryvnia -1 -R01720 - - - - -Финляндская марка -Finnish Marka -100 -R01740 -246 -FIM - - -Французский франк -French Franc -1000 -R01750 -250 -FRF - - -Чешская крона -Czech Koruna -10 -R01760 -203 -CZK - - -Шведская крона -Swedish Krona -10 -R01770 -752 -SEK - - -Швейцарский франк -Swiss Franc -1 -R01775 -756 -CHF - - -ЭКЮ -ECU -1 -R01790 -954 -XEU - - -Эстонская крона -Estonian Kroon -10 -R01795 -233 -EEK - - -Югославский новый динар -Yugoslavian Dinar -1 -R01804 -890 -YUN - - -Сербский динар -Serbian Dinar -100 -R01804 -941 -RSD - - -Южноафриканский рэнд -S.African Rand -10 -R01810 -710 -ZAR - - -Вон Республики Корея -South Korean Won -1000 -R01815 -410 -KRW - - -Японская иена -Japanese Yen -100 -R01820 -392 -JPY - -