Merge branch 'main' into perf/delete-backend-avatars
@ -58,6 +58,12 @@ SECRET_KEY = 'django-insecure-6^&6uw$b5^en%(cu2kc7_o)(mgpazx#j_znwlym0vxfamn2uo-
|
|||||||
# e.g. robotestagw3dcxmd66r4rgksb4nmmr43fh77bzn2ia2eucduyeafnyd.onion
|
# e.g. robotestagw3dcxmd66r4rgksb4nmmr43fh77bzn2ia2eucduyeafnyd.onion
|
||||||
ONION_LOCATION = ''
|
ONION_LOCATION = ''
|
||||||
|
|
||||||
|
# Geoblocked countries (will reject F2F trades).
|
||||||
|
# List of A3 country codes (see fhttps://en.wikipedia.org/wiki/ISO_3166-1_alpha-3)
|
||||||
|
# Leave empty '' to allow all countries.
|
||||||
|
# Example 'NOR,USA,CZE'.
|
||||||
|
GEOBLOCKED_COUNTRIES = 'ABW,AFG,AGO'
|
||||||
|
|
||||||
# Link to robosats alternative site (shown in frontend in statsfornerds so users can switch mainnet/testnet)
|
# Link to robosats alternative site (shown in frontend in statsfornerds so users can switch mainnet/testnet)
|
||||||
ALTERNATIVE_SITE = 'RoboSats6tkf3eva7x2voqso3a5wcorsnw34jveyxfqi2fu7oyheasid.onion'
|
ALTERNATIVE_SITE = 'RoboSats6tkf3eva7x2voqso3a5wcorsnw34jveyxfqi2fu7oyheasid.onion'
|
||||||
ALTERNATIVE_NAME = 'RoboSats Mainnet'
|
ALTERNATIVE_NAME = 'RoboSats Mainnet'
|
||||||
|
36
.github/workflows/android-build.yml
vendored
@ -6,6 +6,15 @@ on:
|
|||||||
semver:
|
semver:
|
||||||
required: true
|
required: true
|
||||||
type: string
|
type: string
|
||||||
|
secrets:
|
||||||
|
KEYSTORE:
|
||||||
|
required: true
|
||||||
|
KEY_ALIAS:
|
||||||
|
required: true
|
||||||
|
KEY_PASS:
|
||||||
|
required: true
|
||||||
|
KEY_STORE_PASS:
|
||||||
|
required: true
|
||||||
push:
|
push:
|
||||||
branches: [ "main" ]
|
branches: [ "main" ]
|
||||||
paths: [ "mobile", "frontend" ]
|
paths: [ "mobile", "frontend" ]
|
||||||
@ -54,10 +63,37 @@ jobs:
|
|||||||
- name: Setup Gradle
|
- name: Setup Gradle
|
||||||
uses: gradle/gradle-build-action@v3
|
uses: gradle/gradle-build-action@v3
|
||||||
|
|
||||||
|
- name: Decode Keystore
|
||||||
|
id: decode_keystore
|
||||||
|
uses: timheuer/base64-to-file@v1.2
|
||||||
|
with:
|
||||||
|
fileName: 'keystore.jks'
|
||||||
|
fileDir: './'
|
||||||
|
encodedString: ${{ secrets.KEYSTORE }}
|
||||||
|
|
||||||
- name: 'Build Android Release'
|
- name: 'Build Android Release'
|
||||||
run: |
|
run: |
|
||||||
cd mobile/android
|
cd mobile/android
|
||||||
./gradlew assembleRelease
|
./gradlew assembleRelease
|
||||||
|
env:
|
||||||
|
KEY_ALIAS: ${{ secrets.KEY_ALIAS }}
|
||||||
|
KEY_PASS: ${{ secrets.KEY_PASS }}
|
||||||
|
KEY_STORE_PASS: ${{ secrets.KEY_STORE_PASS }}
|
||||||
|
|
||||||
|
|
||||||
|
- name: 'Check for non-FOSS libraries'
|
||||||
|
run: |
|
||||||
|
wget https://github.com/iBotPeaches/Apktool/releases/download/v2.7.0/apktool_2.7.0.jar
|
||||||
|
wget https://github.com/iBotPeaches/Apktool/raw/master/scripts/linux/apktool
|
||||||
|
# clone the repo
|
||||||
|
git clone https://gitlab.com/IzzyOnDroid/repo.git
|
||||||
|
# create a directory for Apktool and move the apktool* files there
|
||||||
|
mkdir -p repo/lib/radar/tool
|
||||||
|
mv apktool* repo/lib/radar/tool
|
||||||
|
# create an alias for ease of use
|
||||||
|
chmod u+x repo/lib/radar/tool/apktool
|
||||||
|
mv repo/lib/radar/tool/apktool_2.7.0.jar repo/lib/radar/tool/apktool.jar
|
||||||
|
repo/bin/scanapk.php mobile/android/app/build/outputs/apk/release/app-universal-release.apk
|
||||||
|
|
||||||
- name: 'Get Commit Hash'
|
- name: 'Get Commit Hash'
|
||||||
id: commit
|
id: commit
|
||||||
|
2
.github/workflows/integration-tests.yml
vendored
@ -22,7 +22,7 @@ jobs:
|
|||||||
matrix:
|
matrix:
|
||||||
python-tag: ['3.12.3-slim-bookworm', '3.13-rc-slim-bookworm']
|
python-tag: ['3.12.3-slim-bookworm', '3.13-rc-slim-bookworm']
|
||||||
lnd-version: ['v0.17.4-beta']
|
lnd-version: ['v0.17.4-beta']
|
||||||
cln-version: ['v23.11.2','v24.02']
|
cln-version: ['v23.11.2'] #,'v24.02']
|
||||||
ln-vendor: ['LND'] #, 'CLN']
|
ln-vendor: ['LND'] #, 'CLN']
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
|
1
.github/workflows/release.yml
vendored
@ -71,6 +71,7 @@ jobs:
|
|||||||
android-build:
|
android-build:
|
||||||
uses: RoboSats/robosats/.github/workflows/android-build.yml@main
|
uses: RoboSats/robosats/.github/workflows/android-build.yml@main
|
||||||
needs: [frontend-build, check-versions]
|
needs: [frontend-build, check-versions]
|
||||||
|
secrets: inherit
|
||||||
with:
|
with:
|
||||||
semver: ${{ needs.check-versions.outputs.semver }}
|
semver: ${{ needs.check-versions.outputs.semver }}
|
||||||
|
|
||||||
|
3
.gitignore
vendored
@ -1,9 +1,6 @@
|
|||||||
*.py[cod]
|
*.py[cod]
|
||||||
__pycache__
|
__pycache__
|
||||||
|
|
||||||
# C extensions
|
|
||||||
*.so
|
|
||||||
|
|
||||||
# Packages
|
# Packages
|
||||||
*.egg
|
*.egg
|
||||||
*.egg-info
|
*.egg-info
|
||||||
|
64
api/admin.py
@ -52,8 +52,19 @@ class ETokenAdmin(AdminChangeLinksMixin, TokenAdmin):
|
|||||||
change_links = ("user",)
|
change_links = ("user",)
|
||||||
|
|
||||||
|
|
||||||
|
class LNPaymentInline(admin.StackedInline):
|
||||||
|
model = LNPayment
|
||||||
|
can_delete = True
|
||||||
|
fields = ("num_satoshis", "status", "routing_budget_sats", "description")
|
||||||
|
readonly_fields = ("num_satoshis", "status", "routing_budget_sats", "description")
|
||||||
|
show_change_link = True
|
||||||
|
show_full_result_count = True
|
||||||
|
extra = 0
|
||||||
|
|
||||||
|
|
||||||
@admin.register(Order)
|
@admin.register(Order)
|
||||||
class OrderAdmin(AdminChangeLinksMixin, admin.ModelAdmin):
|
class OrderAdmin(AdminChangeLinksMixin, admin.ModelAdmin):
|
||||||
|
inlines = [LNPaymentInline]
|
||||||
list_display = (
|
list_display = (
|
||||||
"id",
|
"id",
|
||||||
"type",
|
"type",
|
||||||
@ -358,8 +369,61 @@ class OrderAdmin(AdminChangeLinksMixin, admin.ModelAdmin):
|
|||||||
return float(obj.amount)
|
return float(obj.amount)
|
||||||
|
|
||||||
|
|
||||||
|
class OrderInline(admin.StackedInline):
|
||||||
|
model = Order
|
||||||
|
can_delete = False
|
||||||
|
show_change_link = True
|
||||||
|
extra = 0
|
||||||
|
fields = (
|
||||||
|
"id",
|
||||||
|
"type",
|
||||||
|
"maker",
|
||||||
|
"taker",
|
||||||
|
"status",
|
||||||
|
"amount",
|
||||||
|
"currency",
|
||||||
|
"last_satoshis",
|
||||||
|
"is_disputed",
|
||||||
|
"is_fiat_sent",
|
||||||
|
"created_at",
|
||||||
|
"expires_at",
|
||||||
|
"payout_tx",
|
||||||
|
"payout",
|
||||||
|
"maker_bond",
|
||||||
|
"taker_bond",
|
||||||
|
"trade_escrow",
|
||||||
|
)
|
||||||
|
readonly_fields = fields
|
||||||
|
|
||||||
|
|
||||||
|
class PayoutOrderInline(OrderInline):
|
||||||
|
verbose_name = "Order Paid"
|
||||||
|
fk_name = "payout"
|
||||||
|
|
||||||
|
|
||||||
|
class MakerBondOrderInline(OrderInline):
|
||||||
|
verbose_name = "Order Made"
|
||||||
|
fk_name = "maker_bond"
|
||||||
|
|
||||||
|
|
||||||
|
class TakerBondOrderInline(OrderInline):
|
||||||
|
verbose_name = "Order Taken"
|
||||||
|
fk_name = "taker_bond"
|
||||||
|
|
||||||
|
|
||||||
|
class EscrowOrderInline(OrderInline):
|
||||||
|
verbose_name = "Order Escrow"
|
||||||
|
fk_name = "trade_escrow"
|
||||||
|
|
||||||
|
|
||||||
@admin.register(LNPayment)
|
@admin.register(LNPayment)
|
||||||
class LNPaymentAdmin(AdminChangeLinksMixin, admin.ModelAdmin):
|
class LNPaymentAdmin(AdminChangeLinksMixin, admin.ModelAdmin):
|
||||||
|
inlines = [
|
||||||
|
PayoutOrderInline,
|
||||||
|
MakerBondOrderInline,
|
||||||
|
TakerBondOrderInline,
|
||||||
|
EscrowOrderInline,
|
||||||
|
]
|
||||||
list_display = (
|
list_display = (
|
||||||
"hash",
|
"hash",
|
||||||
"concept",
|
"concept",
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import math
|
import math
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
|
||||||
from decouple import config
|
from decouple import config, Csv
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from django.db.models import Q, Sum
|
from django.db.models import Q, Sum
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
@ -9,7 +9,7 @@ from django.utils import timezone
|
|||||||
from api.lightning.node import LNNode
|
from api.lightning.node import LNNode
|
||||||
from api.models import Currency, LNPayment, MarketTick, OnchainPayment, Order
|
from api.models import Currency, LNPayment, MarketTick, OnchainPayment, Order
|
||||||
from api.tasks import send_devfund_donation, send_notification
|
from api.tasks import send_devfund_donation, send_notification
|
||||||
from api.utils import get_minning_fee, validate_onchain_address
|
from api.utils import get_minning_fee, validate_onchain_address, location_country
|
||||||
from chat.models import Message
|
from chat.models import Message
|
||||||
|
|
||||||
FEE = float(config("FEE"))
|
FEE = float(config("FEE"))
|
||||||
@ -29,6 +29,8 @@ MAX_MINING_NETWORK_SPEEDUP_EXPECTED = float(
|
|||||||
config("MAX_MINING_NETWORK_SPEEDUP_EXPECTED")
|
config("MAX_MINING_NETWORK_SPEEDUP_EXPECTED")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
GEOBLOCKED_COUNTRIES = config("GEOBLOCKED_COUNTRIES", cast=Csv(), default="")
|
||||||
|
|
||||||
|
|
||||||
class Logics:
|
class Logics:
|
||||||
@classmethod
|
@classmethod
|
||||||
@ -137,6 +139,19 @@ class Logics:
|
|||||||
|
|
||||||
return True, None
|
return True, None
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def validate_location(cls, order) -> bool:
|
||||||
|
if not (order.latitude or order.longitude):
|
||||||
|
return True, None
|
||||||
|
|
||||||
|
country = location_country(order.longitude, order.latitude)
|
||||||
|
if country in GEOBLOCKED_COUNTRIES:
|
||||||
|
return False, {
|
||||||
|
"bad_request": f"The coordinator does not support orders in {country}"
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
return True, None
|
||||||
|
|
||||||
def validate_amount_within_range(order, amount):
|
def validate_amount_within_range(order, amount):
|
||||||
if amount > float(order.max_amount) or amount < float(order.min_amount):
|
if amount > float(order.max_amount) or amount < float(order.min_amount):
|
||||||
return False, {
|
return False, {
|
||||||
@ -878,7 +893,7 @@ class Logics:
|
|||||||
if order.status == Order.Status.FAI:
|
if order.status == Order.Status.FAI:
|
||||||
if order.payout.status != LNPayment.Status.EXPIRE:
|
if order.payout.status != LNPayment.Status.EXPIRE:
|
||||||
return False, {
|
return False, {
|
||||||
"bad_request": "You can only submit an invoice after expiration or 3 failed attempts"
|
"bad_invoice": "You can only submit an invoice after expiration or 3 failed attempts"
|
||||||
}
|
}
|
||||||
|
|
||||||
# cancel onchain_payout if existing
|
# cancel onchain_payout if existing
|
||||||
@ -894,25 +909,24 @@ class Logics:
|
|||||||
if not payout["valid"]:
|
if not payout["valid"]:
|
||||||
return False, payout["context"]
|
return False, payout["context"]
|
||||||
|
|
||||||
order.payout, _ = LNPayment.objects.update_or_create(
|
if order.payout:
|
||||||
|
if order.payout.payment_hash == payout["payment_hash"]:
|
||||||
|
return False, {"bad_invoice": "You must submit a NEW invoice"}
|
||||||
|
|
||||||
|
order.payout = LNPayment.objects.create(
|
||||||
concept=LNPayment.Concepts.PAYBUYER,
|
concept=LNPayment.Concepts.PAYBUYER,
|
||||||
type=LNPayment.Types.NORM,
|
type=LNPayment.Types.NORM,
|
||||||
sender=User.objects.get(username=ESCROW_USERNAME),
|
sender=User.objects.get(username=ESCROW_USERNAME),
|
||||||
# In case this user has other payouts, update the one related to this order.
|
|
||||||
order_paid_LN=order,
|
|
||||||
receiver=user,
|
receiver=user,
|
||||||
routing_budget_ppm=routing_budget_ppm,
|
routing_budget_ppm=routing_budget_ppm,
|
||||||
routing_budget_sats=routing_budget_sats,
|
routing_budget_sats=routing_budget_sats,
|
||||||
# if there is a LNPayment matching these above, it updates that one with defaults below.
|
invoice=invoice,
|
||||||
defaults={
|
status=LNPayment.Status.VALIDI,
|
||||||
"invoice": invoice,
|
num_satoshis=num_satoshis,
|
||||||
"status": LNPayment.Status.VALIDI,
|
description=payout["description"],
|
||||||
"num_satoshis": num_satoshis,
|
payment_hash=payout["payment_hash"],
|
||||||
"description": payout["description"],
|
created_at=payout["created_at"],
|
||||||
"payment_hash": payout["payment_hash"],
|
expires_at=payout["expires_at"],
|
||||||
"created_at": payout["created_at"],
|
|
||||||
"expires_at": payout["expires_at"],
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
|
|
||||||
order.is_swap = False
|
order.is_swap = False
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import json
|
import json
|
||||||
|
|
||||||
|
from decimal import Decimal
|
||||||
from django.core.validators import MinValueValidator
|
from django.core.validators import MinValueValidator
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
@ -18,7 +19,7 @@ class Currency(models.Model):
|
|||||||
decimal_places=4,
|
decimal_places=4,
|
||||||
default=None,
|
default=None,
|
||||||
null=True,
|
null=True,
|
||||||
validators=[MinValueValidator(0)],
|
validators=[MinValueValidator(Decimal(0))],
|
||||||
)
|
)
|
||||||
timestamp = models.DateTimeField(default=timezone.now)
|
timestamp = models.DateTimeField(default=timezone.now)
|
||||||
|
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
|
from decimal import Decimal
|
||||||
from decouple import config
|
from decouple import config
|
||||||
from django.core.validators import MaxValueValidator, MinValueValidator
|
from django.core.validators import MaxValueValidator, MinValueValidator
|
||||||
from django.db import models
|
from django.db import models
|
||||||
@ -27,21 +28,24 @@ class MarketTick(models.Model):
|
|||||||
decimal_places=2,
|
decimal_places=2,
|
||||||
default=None,
|
default=None,
|
||||||
null=True,
|
null=True,
|
||||||
validators=[MinValueValidator(0)],
|
validators=[MinValueValidator(Decimal(0))],
|
||||||
)
|
)
|
||||||
volume = models.DecimalField(
|
volume = models.DecimalField(
|
||||||
max_digits=8,
|
max_digits=8,
|
||||||
decimal_places=8,
|
decimal_places=8,
|
||||||
default=None,
|
default=None,
|
||||||
null=True,
|
null=True,
|
||||||
validators=[MinValueValidator(0)],
|
validators=[MinValueValidator(Decimal(0))],
|
||||||
)
|
)
|
||||||
premium = models.DecimalField(
|
premium = models.DecimalField(
|
||||||
max_digits=5,
|
max_digits=5,
|
||||||
decimal_places=2,
|
decimal_places=2,
|
||||||
default=None,
|
default=None,
|
||||||
null=True,
|
null=True,
|
||||||
validators=[MinValueValidator(-100), MaxValueValidator(999)],
|
validators=[
|
||||||
|
MinValueValidator(Decimal(-100)),
|
||||||
|
MaxValueValidator(Decimal(999))
|
||||||
|
],
|
||||||
blank=True,
|
blank=True,
|
||||||
)
|
)
|
||||||
currency = models.ForeignKey("api.Currency", null=True, on_delete=models.SET_NULL)
|
currency = models.ForeignKey("api.Currency", null=True, on_delete=models.SET_NULL)
|
||||||
@ -52,7 +56,10 @@ class MarketTick(models.Model):
|
|||||||
max_digits=4,
|
max_digits=4,
|
||||||
decimal_places=4,
|
decimal_places=4,
|
||||||
default=0,
|
default=0,
|
||||||
validators=[MinValueValidator(0), MaxValueValidator(1)],
|
validators=[
|
||||||
|
MinValueValidator(Decimal(0)),
|
||||||
|
MaxValueValidator(Decimal(1))
|
||||||
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
def log_a_tick(order):
|
def log_a_tick(order):
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
from decimal import Decimal
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from django.core.validators import MaxValueValidator, MinValueValidator
|
from django.core.validators import MaxValueValidator, MinValueValidator
|
||||||
@ -58,7 +59,10 @@ class OnchainPayment(models.Model):
|
|||||||
default=2.05,
|
default=2.05,
|
||||||
null=False,
|
null=False,
|
||||||
blank=False,
|
blank=False,
|
||||||
validators=[MinValueValidator(1), MaxValueValidator(999)],
|
validators=[
|
||||||
|
MinValueValidator(Decimal(1)),
|
||||||
|
MaxValueValidator(Decimal(999))
|
||||||
|
],
|
||||||
)
|
)
|
||||||
mining_fee_rate = models.DecimalField(
|
mining_fee_rate = models.DecimalField(
|
||||||
max_digits=6,
|
max_digits=6,
|
||||||
@ -66,7 +70,10 @@ class OnchainPayment(models.Model):
|
|||||||
default=2.05,
|
default=2.05,
|
||||||
null=False,
|
null=False,
|
||||||
blank=False,
|
blank=False,
|
||||||
validators=[MinValueValidator(1), MaxValueValidator(999)],
|
validators=[
|
||||||
|
MinValueValidator(Decimal(1)),
|
||||||
|
MaxValueValidator(Decimal(999))
|
||||||
|
],
|
||||||
)
|
)
|
||||||
mining_fee_sats = models.PositiveBigIntegerField(default=0, null=False, blank=False)
|
mining_fee_sats = models.PositiveBigIntegerField(default=0, null=False, blank=False)
|
||||||
|
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
# We use custom seeded UUID generation during testing
|
# We use custom seeded UUID generation during testing
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
|
from decimal import Decimal
|
||||||
from decouple import config
|
from decouple import config
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
@ -90,7 +91,10 @@ class Order(models.Model):
|
|||||||
decimal_places=2,
|
decimal_places=2,
|
||||||
default=0,
|
default=0,
|
||||||
null=True,
|
null=True,
|
||||||
validators=[MinValueValidator(-100), MaxValueValidator(999)],
|
validators=[
|
||||||
|
MinValueValidator(Decimal(-100)),
|
||||||
|
MaxValueValidator(Decimal(999))
|
||||||
|
],
|
||||||
blank=True,
|
blank=True,
|
||||||
)
|
)
|
||||||
# explicit
|
# explicit
|
||||||
@ -135,8 +139,8 @@ class Order(models.Model):
|
|||||||
default=settings.DEFAULT_BOND_SIZE,
|
default=settings.DEFAULT_BOND_SIZE,
|
||||||
null=False,
|
null=False,
|
||||||
validators=[
|
validators=[
|
||||||
MinValueValidator(settings.MIN_BOND_SIZE), # 2 %
|
MinValueValidator(Decimal(settings.MIN_BOND_SIZE)), # 2 %
|
||||||
MaxValueValidator(settings.MAX_BOND_SIZE), # 15 %
|
MaxValueValidator(Decimal(settings.MAX_BOND_SIZE)), # 15 %
|
||||||
],
|
],
|
||||||
blank=False,
|
blank=False,
|
||||||
)
|
)
|
||||||
@ -147,8 +151,8 @@ class Order(models.Model):
|
|||||||
decimal_places=6,
|
decimal_places=6,
|
||||||
null=True,
|
null=True,
|
||||||
validators=[
|
validators=[
|
||||||
MinValueValidator(-90),
|
MinValueValidator(Decimal(-90)),
|
||||||
MaxValueValidator(90),
|
MaxValueValidator(Decimal(90)),
|
||||||
],
|
],
|
||||||
blank=True,
|
blank=True,
|
||||||
)
|
)
|
||||||
@ -157,8 +161,8 @@ class Order(models.Model):
|
|||||||
decimal_places=6,
|
decimal_places=6,
|
||||||
null=True,
|
null=True,
|
||||||
validators=[
|
validators=[
|
||||||
MinValueValidator(-180),
|
MinValueValidator(Decimal(-180)),
|
||||||
MaxValueValidator(180),
|
MaxValueValidator(Decimal(180)),
|
||||||
],
|
],
|
||||||
blank=True,
|
blank=True,
|
||||||
)
|
)
|
||||||
|
@ -219,14 +219,17 @@ class OrderViewSchema:
|
|||||||
- `update_invoice`
|
- `update_invoice`
|
||||||
- This action only is valid if you are the buyer. The `invoice`
|
- This action only is valid if you are the buyer. The `invoice`
|
||||||
field needs to be present in the body and the value must be a
|
field needs to be present in the body and the value must be a
|
||||||
valid LN invoice as cleartext PGP message signed with the robot key. Make sure to perform this action only when
|
valid LN invoice as cleartext PGP message signed (SHA512) with the robot key.
|
||||||
|
The amount of the invoice should be `invoice_amount` minus the routing
|
||||||
|
budget whose parts per million should be specified by `routing_budget_ppm`.
|
||||||
|
Make sure to perform this action only when
|
||||||
both the bonds are locked. i.e The status of your order is
|
both the bonds are locked. i.e The status of your order is
|
||||||
at least `6` (Waiting for trade collateral and buyer invoice)
|
at least `6` (Waiting for trade collateral and buyer invoice)
|
||||||
- `update_address`
|
- `update_address`
|
||||||
- This action is only valid if you are the buyer. This action is
|
- This action is only valid if you are the buyer. This action is
|
||||||
used to set an on-chain payout address if you wish to have your
|
used to set an on-chain payout address if you wish to have your
|
||||||
payout be received on-chain. Only valid if there is an address in the body as
|
payout be received on-chain. Only valid if there is an address in the body as
|
||||||
cleartext PGP message signed with the robot key. This enables on-chain swap for the
|
cleartext PGP message signed (SHA512) with the robot key. This enables on-chain swap for the
|
||||||
order, so even if you earlier had submitted a LN invoice, it
|
order, so even if you earlier had submitted a LN invoice, it
|
||||||
will be ignored. You get to choose the `mining_fee_rate` as
|
will be ignored. You get to choose the `mining_fee_rate` as
|
||||||
well. Mining fee rate is specified in sats/vbyte.
|
well. Mining fee rate is specified in sats/vbyte.
|
||||||
@ -246,9 +249,7 @@ class OrderViewSchema:
|
|||||||
mid-trade so use this action carefully:
|
mid-trade so use this action carefully:
|
||||||
|
|
||||||
- As a maker if you cancel an order after you have locked your
|
- As a maker if you cancel an order after you have locked your
|
||||||
maker bond, you are returned your bond. This may change in
|
maker bond, you are returned your bond.
|
||||||
the future to prevent DDoSing the LN node and you won't be
|
|
||||||
returned the maker bond.
|
|
||||||
- As a taker there is a time penalty involved if you `take` an
|
- As a taker there is a time penalty involved if you `take` an
|
||||||
order and cancel it without locking the taker bond.
|
order and cancel it without locking the taker bond.
|
||||||
- For both taker or maker, if you cancel the order when both
|
- For both taker or maker, if you cancel the order when both
|
||||||
@ -387,12 +388,13 @@ class RobotViewSchema:
|
|||||||
An authenticated request (has the token's sha256 hash encoded as base 91 in the Authorization header) will be
|
An authenticated request (has the token's sha256 hash encoded as base 91 in the Authorization header) will be
|
||||||
returned the information about the state of a robot.
|
returned the information about the state of a robot.
|
||||||
|
|
||||||
Make sure you generate your token using cryptographically secure methods. [Here's]() the function the Javascript
|
Make sure you generate your token using cryptographically secure methods.
|
||||||
client uses to generate the tokens. Since the server only receives the hash of the
|
Since the server only receives the hash of the
|
||||||
token, it is responsibility of the client to create a strong token. Check
|
token, it is responsibility of the client to create a strong token. Check
|
||||||
[here](https://github.com/RoboSats/robosats/blob/main/frontend/src/utils/token.js)
|
[here](https://github.com/RoboSats/robosats/blob/main/frontend/src/utils/token.ts)
|
||||||
to see how the Javascript client creates a random strong token and how it validates entropy is optimal for tokens
|
to see how the Javascript client creates a random strong token and how it validates entropy is optimal for tokens
|
||||||
created by the user at will.
|
created by the user at will.
|
||||||
|
The PGP key should be an EdDSA ed25519/cert,sign+cv25519/encr key.
|
||||||
|
|
||||||
`public_key` - PGP key associated with the user (Armored ASCII format)
|
`public_key` - PGP key associated with the user (Armored ASCII format)
|
||||||
`encrypted_private_key` - Private PGP key. This is only stored on the backend for later fetching by
|
`encrypted_private_key` - Private PGP key. This is only stored on the backend for later fetching by
|
||||||
@ -403,7 +405,7 @@ class RobotViewSchema:
|
|||||||
A gpg key can be created by:
|
A gpg key can be created by:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
gpg --full-gen-key
|
gpg --default-new-key-algo "ed25519/cert,sign+cv25519/encr" --full-gen-key
|
||||||
```
|
```
|
||||||
|
|
||||||
it's public key can be exported in ascii armored format with:
|
it's public key can be exported in ascii armored format with:
|
||||||
@ -531,7 +533,7 @@ class InfoViewSchema:
|
|||||||
class RewardViewSchema:
|
class RewardViewSchema:
|
||||||
post = {
|
post = {
|
||||||
"summary": "Withdraw reward",
|
"summary": "Withdraw reward",
|
||||||
"description": "Withdraw user reward by submitting an invoice. The invoice must be send as cleartext PGP message signed with the robot key",
|
"description": "Withdraw user reward by submitting an invoice. The invoice must be send as cleartext PGP message signed (SHA512) with the robot key",
|
||||||
"responses": {
|
"responses": {
|
||||||
200: {
|
200: {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
|
@ -494,7 +494,8 @@ class OrderPublicSerializer(serializers.ModelSerializer):
|
|||||||
maker_nick = serializers.CharField(required=False)
|
maker_nick = serializers.CharField(required=False)
|
||||||
maker_hash_id = serializers.CharField(required=False)
|
maker_hash_id = serializers.CharField(required=False)
|
||||||
maker_status = serializers.CharField(
|
maker_status = serializers.CharField(
|
||||||
help_text='Status of the nick - "Active" or "Inactive"', required=False
|
help_text='Status of the nick - "Active", "Seen Recently" or "Inactive"',
|
||||||
|
required=False,
|
||||||
)
|
)
|
||||||
price = serializers.FloatField(
|
price = serializers.FloatField(
|
||||||
help_text="Price in order's fiat currency", required=False
|
help_text="Price in order's fiat currency", required=False
|
||||||
|
27
api/utils.py
@ -479,6 +479,33 @@ def is_valid_token(token: str) -> bool:
|
|||||||
return all(c in charset for c in token)
|
return all(c in charset for c in token)
|
||||||
|
|
||||||
|
|
||||||
|
def location_country(lon: float, lat: float) -> str:
|
||||||
|
"""
|
||||||
|
Returns the country code of a lon/lat location
|
||||||
|
"""
|
||||||
|
|
||||||
|
from shapely.geometry import shape, Point
|
||||||
|
from shapely.prepared import prep
|
||||||
|
|
||||||
|
# Load the GeoJSON data from a local file
|
||||||
|
with open("frontend/static/assets/geo/countries-coastline-10km.geo.json") as f:
|
||||||
|
countries_geojeson = json.load(f)
|
||||||
|
|
||||||
|
# Prepare the countries for reverse geocoding
|
||||||
|
countries = {}
|
||||||
|
for feature in countries_geojeson["features"]:
|
||||||
|
geom = feature["geometry"]
|
||||||
|
country_code = feature["properties"]["A3"]
|
||||||
|
countries[country_code] = prep(shape(geom))
|
||||||
|
|
||||||
|
point = Point(lon, lat)
|
||||||
|
for country_code, geom in countries.items():
|
||||||
|
if geom.contains(point):
|
||||||
|
return country_code
|
||||||
|
|
||||||
|
return "unknown"
|
||||||
|
|
||||||
|
|
||||||
def objects_to_hyperlinks(logs: str) -> str:
|
def objects_to_hyperlinks(logs: str) -> str:
|
||||||
"""
|
"""
|
||||||
Parses strings that have Object(ID,NAME) that match API models.
|
Parses strings that have Object(ID,NAME) that match API models.
|
||||||
|
@ -162,6 +162,10 @@ class MakerView(CreateAPIView):
|
|||||||
if not valid:
|
if not valid:
|
||||||
return Response(context, status.HTTP_400_BAD_REQUEST)
|
return Response(context, status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
valid, context = Logics.validate_location(order)
|
||||||
|
if not valid:
|
||||||
|
return Response(context, status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
order.save()
|
order.save()
|
||||||
order.log(
|
order.log(
|
||||||
f"Order({order.id},{order}) created by Robot({request.user.robot.id},{request.user})"
|
f"Order({order.id},{order}) created by Robot({request.user.robot.id},{request.user})"
|
||||||
|
@ -90,7 +90,7 @@ GEM
|
|||||||
activesupport (>= 2)
|
activesupport (>= 2)
|
||||||
nokogiri (>= 1.4)
|
nokogiri (>= 1.4)
|
||||||
http_parser.rb (0.8.0)
|
http_parser.rb (0.8.0)
|
||||||
i18n (1.14.1)
|
i18n (1.14.4)
|
||||||
concurrent-ruby (~> 1.0)
|
concurrent-ruby (~> 1.0)
|
||||||
jekyll (3.9.5)
|
jekyll (3.9.5)
|
||||||
addressable (~> 2.4)
|
addressable (~> 2.4)
|
||||||
@ -213,7 +213,7 @@ GEM
|
|||||||
jekyll (>= 3.5, < 5.0)
|
jekyll (>= 3.5, < 5.0)
|
||||||
jekyll-feed (~> 0.9)
|
jekyll-feed (~> 0.9)
|
||||||
jekyll-seo-tag (~> 2.1)
|
jekyll-seo-tag (~> 2.1)
|
||||||
minimal-mistakes-jekyll (4.24.0)
|
minimal-mistakes-jekyll (4.25.1)
|
||||||
jekyll (>= 3.7, < 5.0)
|
jekyll (>= 3.7, < 5.0)
|
||||||
jekyll-feed (~> 0.1)
|
jekyll-feed (~> 0.1)
|
||||||
jekyll-gist (~> 1.5)
|
jekyll-gist (~> 1.5)
|
||||||
@ -230,7 +230,7 @@ GEM
|
|||||||
sawyer (~> 0.9)
|
sawyer (~> 0.9)
|
||||||
pathutil (0.16.2)
|
pathutil (0.16.2)
|
||||||
forwardable-extended (~> 2.6)
|
forwardable-extended (~> 2.6)
|
||||||
public_suffix (5.0.4)
|
public_suffix (5.0.5)
|
||||||
racc (1.7.3)
|
racc (1.7.3)
|
||||||
rb-fsevent (0.11.2)
|
rb-fsevent (0.11.2)
|
||||||
rb-inotify (0.10.1)
|
rb-inotify (0.10.1)
|
||||||
|
@ -62,6 +62,14 @@ In Canada, [Interac e-Transfer](https://www.interac.ca/en/consumers/support/faq-
|
|||||||
|
|
||||||
The best practice for users trying to transact with a payment method with a high risk of losing funds is discussed in this section.
|
The best practice for users trying to transact with a payment method with a high risk of losing funds is discussed in this section.
|
||||||
|
|
||||||
|
### Instant SEPA Payment Guidelines
|
||||||
|
|
||||||
|
Instant SEPA is a widely adopted payment method across Europe, offering fast and efficient cashless transactions. However, it comes with a significant risk for sellers, including the potential for chargebacks. To mitigate these risks, it is advisable for sellers to request the buyer's information before sharing their SEPA details. This information could include the buyer's country, full name, and bank account number. By obtaining this information, sellers can reduce the risk of fraudulent transactions, such as triangle attacks, while buyers, sharing this information does not decrease their privacy, as they are not exposing any additional information that the seller would not have access to anyway after the SEPA transfer.
|
||||||
|
|
||||||
|
For buyers, it is crucial to comply with sellers' if they request personal information when they are initiating SEPA transactions. Failure to provide this information can lead to the seller raising an immediate dispute, which sellers are likely to win (the seller will also earn the buyer's bond in this specific case). Therefore, it is in the best interest of buyers to cooperate with sellers' requests for information.
|
||||||
|
|
||||||
|
Sellers are encouraged to share a link to this guide with their buyers when requesting information. This ensures that both parties are informed and understand the importance of this step when using Instant SEPA.
|
||||||
|
|
||||||
### Revolut via payment links
|
### Revolut via payment links
|
||||||
|
|
||||||
In a Revolut payment, a `@revtag` is usually exchanged in the chat and can be verified in the payment history of the app making proof of payments easy.
|
In a Revolut payment, a `@revtag` is usually exchanged in the chat and can be verified in the payment history of the app making proof of payments easy.
|
||||||
@ -73,9 +81,11 @@ In a dispute, there's no recipient address reference and both buyer and seller c
|
|||||||
Therefore, insist on receiving the `@revtag` when making a payment with Revolut to avoid these risks. The `@revtag` can also be received as a link. This link would look like this: https://revolut.me/@revtag.
|
Therefore, insist on receiving the `@revtag` when making a payment with Revolut to avoid these risks. The `@revtag` can also be received as a link. This link would look like this: https://revolut.me/@revtag.
|
||||||
|
|
||||||
### Paypal
|
### Paypal
|
||||||
Paypal is one of the widely used fiat payment methods. However, with <a href="https://www.paypal.com/us/webapps/mpp/ua/buyer-protection">PayPal buyer protection policy</a>, buyer can do fraudulent action by creating a refund request in PayPal after the trading process in RoboSats is finished and therefore taking both fiat and bitcoin all by themselves.
|
Paypal is one of the widely used fiat payment methods. However, as a seller Paypal is the highest risk you can take. Using Paypal as payment method is not advised.
|
||||||
|
|
||||||
This fraud can be prevented by agreeing with the buyer to have them send money using the “send money to a friend or family member” option. This will make the buyer become the one liable for the transaction fee and make it less likely for them to request a refund.
|
If you still wish to use Paypal there is a few things to take into account. With <a href="https://www.paypal.com/us/webapps/mpp/ua/buyer-protection">PayPal buyer protection policy</a>, buyers can do fraudulent action by creating a refund request in PayPal after the trading process in RoboSats is finished and therefore taking both fiat and bitcoin all by themselves.
|
||||||
|
|
||||||
|
This fraud could be prevented by agreeing with the buyer to have them send money using the “send money to a friend or family member” option. This will make the buyer become the one liable for the transaction fee and make it less likely for them to request a refund.
|
||||||
|
|
||||||
### For seller
|
### For seller
|
||||||
If you are a seller and your peer both agreed to use “send money to a friend or family member” but your peer used the "send money for Goods or Services" option, you should return the fiat payment and ask your peer to send with an agreed method. If they insist to break the agreement, you may ask them to voluntarily end the trade or end the trade by calling a dispute.
|
If you are a seller and your peer both agreed to use “send money to a friend or family member” but your peer used the "send money for Goods or Services" option, you should return the fiat payment and ask your peer to send with an agreed method. If they insist to break the agreement, you may ask them to voluntarily end the trade or end the trade by calling a dispute.
|
||||||
@ -83,26 +93,4 @@ If you are a seller and your peer both agreed to use “send money to a friend o
|
|||||||
### For buyer
|
### For buyer
|
||||||
If you are a buyer and you need to use “send money to a friend or family member” to pay fiat to your peer, you can choose the specified payment type by following these steps.
|
If you are a buyer and you need to use “send money to a friend or family member” to pay fiat to your peer, you can choose the specified payment type by following these steps.
|
||||||
|
|
||||||
#### PayPal Desktop
|
|
||||||
In PayPal desktop, it is located below the drop-down currency list, it should be labeled as "Sending to a friend".
|
|
||||||
If it is labeled otherwise, you'll need to click "Change" on the right to change the payment type.
|
|
||||||
<div align="center">
|
|
||||||
<img src="/assets/images/fiat-payment-methods/PayPal-main-desktop.png" width="370"/>
|
|
||||||
</div>
|
|
||||||
Then select "Sending to a friend" in the payment type choosing page.
|
|
||||||
<div align="center">
|
|
||||||
<img src="/assets/images/fiat-payment-methods/PayPal-choose-desktop.png" width="370"/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
#### PayPal Mobile
|
|
||||||
In PayPal mobile, it is located below the payment method (In this case is VISA), it should be labeled as "Friends or Family".
|
|
||||||
If it is labeled otherwise, you'll need to tab ">" on the right to change the payment type.
|
|
||||||
<div align="center">
|
|
||||||
<img src="/assets/images/fiat-payment-methods/PayPal-main-phone.png" width="230"/>
|
|
||||||
</div>
|
|
||||||
Then select "Friends or Family" in the payment type choosing page.
|
|
||||||
<div align="center">
|
|
||||||
<img src="/assets/images/fiat-payment-methods/PayPal-choose-phone.png" width="230"/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{% include improve %}
|
{% include improve %}
|
||||||
|
@ -28,15 +28,17 @@ This is a non-exhaustive compilation based on past experience of users. We have
|
|||||||
| Wallet | Version | Device | UX<sup>1</sup> | Bonds<sup>2</sup> | Payout<sup>3</sup> | Comp<sup>4</sup> | Total<sup>5</sup> |
|
| Wallet | Version | Device | UX<sup>1</sup> | Bonds<sup>2</sup> | Payout<sup>3</sup> | Comp<sup>4</sup> | Total<sup>5</sup> |
|
||||||
|:---|:---|:--:|:--:|:--:|:--:|:--:|:--:|
|
|:---|:---|:--:|:--:|:--:|:--:|:--:|:--:|
|
||||||
|[Alby](#alby-browser-extension)|[v1.14.2](https://github.com/getAlby/lightning-browser-extension)|{{page.laptop}}|{{page.good}}|{{page.good}}|{{page.good}}|{{page.good}} |{{page.thumbsup}}|
|
|[Alby](#alby-browser-extension)|[v1.14.2](https://github.com/getAlby/lightning-browser-extension)|{{page.laptop}}|{{page.good}}|{{page.good}}|{{page.good}}|{{page.good}} |{{page.thumbsup}}|
|
||||||
|
|[Aqua](#aqua-mobile)|[v0.1.55](https://aquawallet.io/)|{{page.phone}}|{{page.good}}|{{page.good}}|{{page.unclear}}|{{page.good}} |{{page.thumbsup}}|
|
||||||
|[Blink](#blink-mobile-former-bitcoin-beach-wallet)|[2.2.73](https://www.blink.sv/)|{{page.phone}}|{{page.good}}|{{page.good}}|{{page.good}}|{{page.good}} |{{page.thumbsup}}|
|
|[Blink](#blink-mobile-former-bitcoin-beach-wallet)|[2.2.73](https://www.blink.sv/)|{{page.phone}}|{{page.good}}|{{page.good}}|{{page.good}}|{{page.good}} |{{page.thumbsup}}|
|
||||||
|[Blixt](#blixt-androidios-lnd-light-backend-on-device)|[v0.4.1](https://github.com/hsjoberg/blixt-wallet)|{{page.phone}}|{{page.soso}}|{{page.good}}|{{page.good}}|{{page.good}}|{{page.thumbsup}}|
|
|[Blixt](#blixt-androidios-lnd-light-backend-on-device)|[v0.4.1](https://github.com/hsjoberg/blixt-wallet)|{{page.phone}}|{{page.soso}}|{{page.good}}|{{page.good}}|{{page.good}}|{{page.thumbsup}}|
|
||||||
|[Blue](#bluewallet-mobile)|[1.4.4](https://bluewallet.io/)|{{page.phone}}|{{page.good}}|{{page.unclear}}|{{page.unclear}}|{{page.good}}|{{page.unclear}}|
|
|[Blue](#bluewallet-mobile)|[1.4.4](https://bluewallet.io/)|{{page.phone}}|{{page.good}}|{{page.unclear}}|{{page.unclear}}|{{page.good}}|{{page.unclear}}|
|
||||||
|[Breez](#breez-mobile)|[0.16](https://breez.technology/mobile/)|{{page.phone}}|{{page.good}}|{{page.good}}|{{page.good}}|{{page.good}}|{{page.thumbsup}}|
|
|[Breez](#breez-mobile)|[0.16](https://breez.technology/mobile/)|{{page.phone}}|{{page.good}}|{{page.good}}|{{page.good}}|{{page.good}}|{{page.thumbsup}}|
|
||||||
|[Cash App](#cash-app-mobile)|[4.7](https://cash.app/)|{{page.phone}}|{{page.good}}|{{page.good}}|{{page.good}}|{{page.good}} |{{page.thumbsup}}|
|
|[Cash App](#cash-app-mobile)|[4.7](https://cash.app/)|{{page.phone}}|{{page.good}}|{{page.good}}|{{page.good}}|{{page.good}} |{{page.thumbsup}}|
|
||||||
|[Core Lightning](#core-lightning--cln-cli-interface)|[v0.11.1](https://github.com/ElementsProject/lightning)|{{page.cli}}|{{page.good}}|{{page.good}}|{{page.good}}|{{page.good}}|{{page.thumbsup}}|
|
|[Core Lightning](#core-lightning--cln-cli-interface)|[v0.11.1](https://github.com/ElementsProject/lightning)|{{page.cli}}|{{page.good}}|{{page.good}}|{{page.good}}|{{page.good}}|{{page.thumbsup}}|
|
||||||
|[Electrum](#electrum-desktop)|[4.1.4](https://github.com/spesmilo/electrum)|{{page.laptop}}|{{page.good}}|{{page.good}}|{{page.good}}|{{page.good}}|{{page.thumbsup}}||
|
|[Electrum](#electrum-mobile--desktop)|[4.1.4](https://github.com/spesmilo/electrum)|{{page.laptop}}{{page.phone}}|{{page.good}}|{{page.good}}|{{page.good}}|{{page.good}}|{{page.thumbsup}}||
|
||||||
|[LND](#lnd-cli-interface)|[v0.14.2](https://github.com/LightningNetwork/lnd)|{{page.cli}}|{{page.good}}|{{page.good}}|{{page.good}}|{{page.good}}|{{page.thumbsup}}|
|
|[LND](#lnd-cli-interface)|[v0.14.2](https://github.com/LightningNetwork/lnd)|{{page.cli}}|{{page.good}}|{{page.good}}|{{page.good}}|{{page.good}}|{{page.thumbsup}}|
|
||||||
|[Mash](https://app.mash.com/wallet)|[Beta](https://mash.com/consumer-experience/)|{{page.laptop}}{{page.phone}}|{{page.good}}|{{page.good}}|{{page.good}}|{{page.good}} | {{page.thumbsup}}|
|
|[Mash](https://app.mash.com/wallet)|[Beta](https://mash.com/consumer-experience/)|{{page.laptop}}{{page.phone}}|{{page.good}}|{{page.good}}|{{page.good}}|{{page.good}} | {{page.thumbsup}}|
|
||||||
|
|[Mutiny](#mutiny-mobile--web-browser-wallet)|[1.7.1](https://www.mutinywallet.com/)|{{page.laptop}}{{page.phone}}|{{page.good}}|{{page.good}}|{{page.good}}|{{page.good}}|{{page.thumbsdown}}||
|
||||||
|[Muun](#muun-mobile)|[47.3](https://muun.com/)|{{page.phone}}|{{page.good}}|{{page.good}}|{{page.bad}}|{{page.bad}}|{{page.thumbsdown}}|
|
|[Muun](#muun-mobile)|[47.3](https://muun.com/)|{{page.phone}}|{{page.good}}|{{page.good}}|{{page.bad}}|{{page.bad}}|{{page.thumbsdown}}|
|
||||||
|[Phoenix](#phoenix-mobile)|[35-1.4.20](https://phoenix.acinq.co/)|{{page.phone}}|{{page.good}}|{{page.soso}}|{{page.soso}}|{{page.soso}}|{{page.unclear}}|
|
|[Phoenix](#phoenix-mobile)|[35-1.4.20](https://phoenix.acinq.co/)|{{page.phone}}|{{page.good}}|{{page.soso}}|{{page.soso}}|{{page.soso}}|{{page.unclear}}|
|
||||||
|[SBW](https://github.com/RoboSats/robosats/issues/44#issue-1135544303)|[2.4.27](https://github.com/btcontract/wallet/)|{{page.phone}}|{{page.good}}|{{page.good}}|{{page.good}}|{{page.good}}|{{page.thumbsup}}|
|
|[SBW](https://github.com/RoboSats/robosats/issues/44#issue-1135544303)|[2.4.27](https://github.com/btcontract/wallet/)|{{page.phone}}|{{page.good}}|{{page.good}}|{{page.good}}|{{page.good}}|{{page.thumbsup}}|
|
||||||
@ -57,6 +59,12 @@ Instructions to install Alby in Tor Browser:
|
|||||||
1. Install the Alby extension from the [Firefox add-ons store](https://addons.mozilla.org/en-US/firefox/addon/alby/)
|
1. Install the Alby extension from the [Firefox add-ons store](https://addons.mozilla.org/en-US/firefox/addon/alby/)
|
||||||
2. Click on the Alby extension and follow the prompts to setup your wallet.
|
2. Click on the Alby extension and follow the prompts to setup your wallet.
|
||||||
|
|
||||||
|
### Aqua (Mobile)
|
||||||
|
Overall the wallet works as expected. Hold invoices work reliably.
|
||||||
|
What is inconvenient:
|
||||||
|
- Lightning payments are encapsulated into Liquid Bitcoin so there is a small additional fee for conversion in/out
|
||||||
|
- Bond refund is locked for 3 days
|
||||||
|
|
||||||
### Blink (Mobile, former Bitcoin Beach Wallet)
|
### Blink (Mobile, former Bitcoin Beach Wallet)
|
||||||
Works well with RoboSats. Hodl invoices (Bonds) show as "Pending" in the transaction history. Payouts to the Blink wallet function as intended. Custodial wallet by Galoy which originated from the Bitcoin Beach project in El Salvador (formerly known as "Bitcoin Beach Wallet").
|
Works well with RoboSats. Hodl invoices (Bonds) show as "Pending" in the transaction history. Payouts to the Blink wallet function as intended. Custodial wallet by Galoy which originated from the Bitcoin Beach project in El Salvador (formerly known as "Bitcoin Beach Wallet").
|
||||||
|
|
||||||
@ -75,8 +83,11 @@ Works well with RoboSats. Hodl invoices (Bonds) show as "Pending" in the transac
|
|||||||
### Core Lightning / CLN (CLI Interface)
|
### Core Lightning / CLN (CLI Interface)
|
||||||
Works as expected. The `lightning-cli pay <invoice>` command does not conclude while the payment is pending, but can use `lightning-cli paystatus <invoice>` to monitor the state.
|
Works as expected. The `lightning-cli pay <invoice>` command does not conclude while the payment is pending, but can use `lightning-cli paystatus <invoice>` to monitor the state.
|
||||||
|
|
||||||
### Electrum (Desktop)
|
### Electrum (Mobile & Desktop)
|
||||||
Works as expected. Some payments and locks may fail depending on the Lightning node the channel is created to. Channels to ASINQ work fine.
|
Overall the wallet works as expected. The interface is precise and clear.
|
||||||
|
What is inconvenient:
|
||||||
|
- all your Lightning channels have to be created to the node: ACINQ
|
||||||
|
|
||||||
|
|
||||||
### LND (CLI Interface)
|
### LND (CLI Interface)
|
||||||
Raw; it shows exactly what is happening and what it knows "IN_FLIGHT". It is not user friendly and therefore not recommended to interact with RoboSats by beginners. However, everything works just fine. If you are using LNCLI regularly, then you will find no issue using it with RoboSats.
|
Raw; it shows exactly what is happening and what it knows "IN_FLIGHT". It is not user friendly and therefore not recommended to interact with RoboSats by beginners. However, everything works just fine. If you are using LNCLI regularly, then you will find no issue using it with RoboSats.
|
||||||
@ -85,6 +96,13 @@ Raw; it shows exactly what is happening and what it knows "IN_FLIGHT". It is not
|
|||||||
### Mash Wallet App (Mobile PWA & Desktop Web-Wallet)
|
### Mash Wallet App (Mobile PWA & Desktop Web-Wallet)
|
||||||
Overall the [Mash](https://mash.com/consumer-experience/) wallet works end2end with Robosats on both selling & buying over lightning. Majority of relevant invoice details in the mash wallet are shown and clear to users throughout the process. When the transactions are complete, they open in the mobile app on both sender/receiver sides to highlight that the transactions are completed.The one UX hick-up is that the pending invoices list doesn't explicitly show HOLD invoices and there is a "spinning" screen on first HOLD invoice payment. The team has a bug open to fix this issue shortly (this note is from Aug 21st 2023).
|
Overall the [Mash](https://mash.com/consumer-experience/) wallet works end2end with Robosats on both selling & buying over lightning. Majority of relevant invoice details in the mash wallet are shown and clear to users throughout the process. When the transactions are complete, they open in the mobile app on both sender/receiver sides to highlight that the transactions are completed.The one UX hick-up is that the pending invoices list doesn't explicitly show HOLD invoices and there is a "spinning" screen on first HOLD invoice payment. The team has a bug open to fix this issue shortly (this note is from Aug 21st 2023).
|
||||||
|
|
||||||
|
### Mutiny (Mobile & Web Browser Wallet)
|
||||||
|
The wallet should work as expected, but the interface, transaction states, and the structure of the funds can sometimes be very confusing in the current release version.
|
||||||
|
Use the default free Fedimint(Chaumian eCash) account, with the possibility to use zero fee Lightning transfers.
|
||||||
|
What is inconvenient:
|
||||||
|
- occasionally wallet restart is needed
|
||||||
|
- more than two pending hold invoices at the same time may cause a rejection of the new transaction
|
||||||
|
|
||||||
### Muun (Mobile)
|
### Muun (Mobile)
|
||||||
Similar to Blixt or LND, Muun plays nicely with hold invoices. You can be a seller in RoboSats using Muun and the user experience will be great. However, in order to be a buyer when using Muun, you need to submit an on-chain address for the payout as a Lightning invoice won't work. Muun is _fee siphoning attacking_ any sender to Muun wallet. There is a mandatory hop through a private channel with a fee of +1500ppm. RoboSats will strictly not route a buyer payout for a net loss. Given that RoboSats trading fees are {{site.robosats.total_fee}}% and it needs to cover the routing fees, **RoboSats will never find a suitable route to a Muun wallet user**. At the moment, RoboSats will scan your invoice for routing hints that can potentially encode a _fee siphoning attack_. If this trick is found, then the invoice will be rejected: submit an on-chain address instead for an on-the-fly swap. Refer to [Understand > On-Chain Payouts](/docs/on-chain-payouts/) for more information about on-the-fly swaps. Important to note that Muun has issues during times of high on chain fee spikes. Regardless, the workaround to receive to Muun is: either submit an on chain address or choose a higher routing budget after enabling the "Advanced Options" switch.
|
Similar to Blixt or LND, Muun plays nicely with hold invoices. You can be a seller in RoboSats using Muun and the user experience will be great. However, in order to be a buyer when using Muun, you need to submit an on-chain address for the payout as a Lightning invoice won't work. Muun is _fee siphoning attacking_ any sender to Muun wallet. There is a mandatory hop through a private channel with a fee of +1500ppm. RoboSats will strictly not route a buyer payout for a net loss. Given that RoboSats trading fees are {{site.robosats.total_fee}}% and it needs to cover the routing fees, **RoboSats will never find a suitable route to a Muun wallet user**. At the moment, RoboSats will scan your invoice for routing hints that can potentially encode a _fee siphoning attack_. If this trick is found, then the invoice will be rejected: submit an on-chain address instead for an on-the-fly swap. Refer to [Understand > On-Chain Payouts](/docs/on-chain-payouts/) for more information about on-the-fly swaps. Important to note that Muun has issues during times of high on chain fee spikes. Regardless, the workaround to receive to Muun is: either submit an on chain address or choose a higher routing budget after enabling the "Advanced Options" switch.
|
||||||
|
|
||||||
@ -93,6 +111,7 @@ One of the simplest and one of the best. The hodl invoice shows as "on fly", it
|
|||||||
*Update 26-10-23: At this moment it has no development or support
|
*Update 26-10-23: At this moment it has no development or support
|
||||||
|
|
||||||
### Phoenix (Mobile)
|
### Phoenix (Mobile)
|
||||||
|
DEV team stated that they do not want to support hold invoices.
|
||||||
*Update 21-10-23. Phoenix used to work as described here, but many things changed to worse with the last update of the wallet.
|
*Update 21-10-23. Phoenix used to work as described here, but many things changed to worse with the last update of the wallet.
|
||||||
Phoenix works very well as an order taker. Phoenix will also work well as an order maker as long as the order settings `public duration` + `deposit duration` are lower than 10 hours. Otherwise, you might have problems locking the maker bond. If the total duraton of bonds/escrow invoices exceeds 450 blocks, then Phoenix will not allow users to lock the bond (`Cannot add htlc (...) reason=expiry too big`).
|
Phoenix works very well as an order taker. Phoenix will also work well as an order maker as long as the order settings `public duration` + `deposit duration` are lower than 10 hours. Otherwise, you might have problems locking the maker bond. If the total duraton of bonds/escrow invoices exceeds 450 blocks, then Phoenix will not allow users to lock the bond (`Cannot add htlc (...) reason=expiry too big`).
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
openapi: 3.0.3
|
openapi: 3.0.3
|
||||||
info:
|
info:
|
||||||
title: RoboSats REST API
|
title: RoboSats REST API
|
||||||
version: 0.6.0
|
version: 0.6.2
|
||||||
x-logo:
|
x-logo:
|
||||||
url: https://raw.githubusercontent.com/Reckless-Satoshi/robosats/main/frontend/static/assets/images/robosats-0.1.1-banner.png
|
url: https://raw.githubusercontent.com/Reckless-Satoshi/robosats/main/frontend/static/assets/images/robosats-0.1.1-banner.png
|
||||||
backgroundColor: '#FFFFFF'
|
backgroundColor: '#FFFFFF'
|
||||||
@ -443,14 +443,17 @@ paths:
|
|||||||
- `update_invoice`
|
- `update_invoice`
|
||||||
- This action only is valid if you are the buyer. The `invoice`
|
- This action only is valid if you are the buyer. The `invoice`
|
||||||
field needs to be present in the body and the value must be a
|
field needs to be present in the body and the value must be a
|
||||||
valid LN invoice as cleartext PGP message signed with the robot key. Make sure to perform this action only when
|
valid LN invoice as cleartext PGP message signed (SHA512) with the robot key.
|
||||||
|
The amount of the invoice should be `invoice_amount` minus the routing
|
||||||
|
budget whose parts per million should be specified by `routing_budget_ppm`.
|
||||||
|
Make sure to perform this action only when
|
||||||
both the bonds are locked. i.e The status of your order is
|
both the bonds are locked. i.e The status of your order is
|
||||||
at least `6` (Waiting for trade collateral and buyer invoice)
|
at least `6` (Waiting for trade collateral and buyer invoice)
|
||||||
- `update_address`
|
- `update_address`
|
||||||
- This action is only valid if you are the buyer. This action is
|
- This action is only valid if you are the buyer. This action is
|
||||||
used to set an on-chain payout address if you wish to have your
|
used to set an on-chain payout address if you wish to have your
|
||||||
payout be received on-chain. Only valid if there is an address in the body as
|
payout be received on-chain. Only valid if there is an address in the body as
|
||||||
cleartext PGP message signed with the robot key. This enables on-chain swap for the
|
cleartext PGP message signed (SHA512) with the robot key. This enables on-chain swap for the
|
||||||
order, so even if you earlier had submitted a LN invoice, it
|
order, so even if you earlier had submitted a LN invoice, it
|
||||||
will be ignored. You get to choose the `mining_fee_rate` as
|
will be ignored. You get to choose the `mining_fee_rate` as
|
||||||
well. Mining fee rate is specified in sats/vbyte.
|
well. Mining fee rate is specified in sats/vbyte.
|
||||||
@ -470,9 +473,7 @@ paths:
|
|||||||
mid-trade so use this action carefully:
|
mid-trade so use this action carefully:
|
||||||
|
|
||||||
- As a maker if you cancel an order after you have locked your
|
- As a maker if you cancel an order after you have locked your
|
||||||
maker bond, you are returned your bond. This may change in
|
maker bond, you are returned your bond.
|
||||||
the future to prevent DDoSing the LN node and you won't be
|
|
||||||
returned the maker bond.
|
|
||||||
- As a taker there is a time penalty involved if you `take` an
|
- As a taker there is a time penalty involved if you `take` an
|
||||||
order and cancel it without locking the taker bond.
|
order and cancel it without locking the taker bond.
|
||||||
- For both taker or maker, if you cancel the order when both
|
- For both taker or maker, if you cancel the order when both
|
||||||
@ -631,7 +632,7 @@ paths:
|
|||||||
post:
|
post:
|
||||||
operationId: reward_create
|
operationId: reward_create
|
||||||
description: Withdraw user reward by submitting an invoice. The invoice must
|
description: Withdraw user reward by submitting an invoice. The invoice must
|
||||||
be send as cleartext PGP message signed with the robot key
|
be send as cleartext PGP message signed (SHA512) with the robot key
|
||||||
summary: Withdraw reward
|
summary: Withdraw reward
|
||||||
tags:
|
tags:
|
||||||
- reward
|
- reward
|
||||||
@ -721,12 +722,13 @@ paths:
|
|||||||
An authenticated request (has the token's sha256 hash encoded as base 91 in the Authorization header) will be
|
An authenticated request (has the token's sha256 hash encoded as base 91 in the Authorization header) will be
|
||||||
returned the information about the state of a robot.
|
returned the information about the state of a robot.
|
||||||
|
|
||||||
Make sure you generate your token using cryptographically secure methods. [Here's]() the function the Javascript
|
Make sure you generate your token using cryptographically secure methods.
|
||||||
client uses to generate the tokens. Since the server only receives the hash of the
|
Since the server only receives the hash of the
|
||||||
token, it is responsibility of the client to create a strong token. Check
|
token, it is responsibility of the client to create a strong token. Check
|
||||||
[here](https://github.com/RoboSats/robosats/blob/main/frontend/src/utils/token.js)
|
[here](https://github.com/RoboSats/robosats/blob/main/frontend/src/utils/token.ts)
|
||||||
to see how the Javascript client creates a random strong token and how it validates entropy is optimal for tokens
|
to see how the Javascript client creates a random strong token and how it validates entropy is optimal for tokens
|
||||||
created by the user at will.
|
created by the user at will.
|
||||||
|
The PGP key should be an EdDSA ed25519/cert,sign+cv25519/encr key.
|
||||||
|
|
||||||
`public_key` - PGP key associated with the user (Armored ASCII format)
|
`public_key` - PGP key associated with the user (Armored ASCII format)
|
||||||
`encrypted_private_key` - Private PGP key. This is only stored on the backend for later fetching by
|
`encrypted_private_key` - Private PGP key. This is only stored on the backend for later fetching by
|
||||||
@ -737,7 +739,7 @@ paths:
|
|||||||
A gpg key can be created by:
|
A gpg key can be created by:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
gpg --full-gen-key
|
gpg --default-new-key-algo "ed25519/cert,sign+cv25519/encr" --full-gen-key
|
||||||
```
|
```
|
||||||
|
|
||||||
it's public key can be exported in ascii armored format with:
|
it's public key can be exported in ascii armored format with:
|
||||||
@ -1636,7 +1638,7 @@ components:
|
|||||||
type: string
|
type: string
|
||||||
maker_status:
|
maker_status:
|
||||||
type: string
|
type: string
|
||||||
description: Status of the nick - "Active" or "Inactive"
|
description: Status of the nick - "Active", "Seen Recently" or "Inactive"
|
||||||
price:
|
price:
|
||||||
type: number
|
type: number
|
||||||
format: double
|
format: double
|
||||||
|
@ -1539,7 +1539,7 @@ components:
|
|||||||
type: string
|
type: string
|
||||||
maker_status:
|
maker_status:
|
||||||
type: string
|
type: string
|
||||||
description: Status of the nick - "Active" or "Inactive"
|
description: Status of the nick - "Active", "Seen Recently" or "Inactive"
|
||||||
price:
|
price:
|
||||||
type: integer
|
type: integer
|
||||||
description: Price in order's fiat currency
|
description: Price in order's fiat currency
|
||||||
|
@ -1712,7 +1712,7 @@ components:
|
|||||||
type: string
|
type: string
|
||||||
maker_status:
|
maker_status:
|
||||||
type: string
|
type: string
|
||||||
description: Status of the nick - "Active" or "Inactive"
|
description: Status of the nick - "Active", "Seen Recently" or "Inactive"
|
||||||
price:
|
price:
|
||||||
type: number
|
type: number
|
||||||
format: double
|
format: double
|
||||||
|
12
fastlane/metadata/en-US/full_description.txt
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<p>RoboSats is a simple and private app to exchange bitcoin for national currencies. Robosats simplifies the P2P user experience and uses lightning hold invoices to minimize custody and trust requirements. The deterministically generated robot avatars help users stick to best privacy practices.</p>
|
||||||
|
<p><br><b>Features:</b></p><ul>
|
||||||
|
<li>Privacy focused: your robot avatar is deterministically generated, no need for registration.</li>
|
||||||
|
<li>More than 10 languages available and over 60 fiat currencies</li>
|
||||||
|
<li>Safe: simply lock a lightning hodl invoice and show you are real and committed.</li>
|
||||||
|
<li>No data collection. Your communication with your peer is PGP encrypted, only you can read it.</li>
|
||||||
|
<li>Lightning fast: the average sovereign trade finishes in ~ 8 minutes. Faster than a single block confirmation!</li>
|
||||||
|
<li>Fully collateralized escrow: your peer is always committed and cannot run away with the funds.</li>
|
||||||
|
<li>Strong incentives system: attempts of cheating are penalized with the slashing of the Sats in the fidelity bond.</li>
|
||||||
|
<li>Guides and video tutorials available at https://learn.robosats.com/watch/en</li>
|
||||||
|
</ul>
|
||||||
|
<p>You can join other cool Robots and get community support at <a href="https://t.me/robosats">our Telegram group</a>.</p>
|
BIN
fastlane/metadata/en-US/images/icon.png
Normal file
After Width: | Height: | Size: 3.0 KiB |
BIN
fastlane/metadata/en-US/images/phoneScreenshots/01.jpg
Normal file
After Width: | Height: | Size: 89 KiB |
BIN
fastlane/metadata/en-US/images/phoneScreenshots/02.jpg
Normal file
After Width: | Height: | Size: 61 KiB |
BIN
fastlane/metadata/en-US/images/phoneScreenshots/03.jpg
Normal file
After Width: | Height: | Size: 48 KiB |
BIN
fastlane/metadata/en-US/images/phoneScreenshots/04.jpg
Normal file
After Width: | Height: | Size: 54 KiB |
BIN
fastlane/metadata/en-US/images/phoneScreenshots/05.jpg
Normal file
After Width: | Height: | Size: 50 KiB |
BIN
fastlane/metadata/en-US/images/phoneScreenshots/06.jpg
Normal file
After Width: | Height: | Size: 56 KiB |
1
fastlane/metadata/en-US/short_description.txt
Normal file
@ -0,0 +1 @@
|
|||||||
|
Simple and private app to exchange bitcoin for national currencies.
|
2545
frontend/package-lock.json
generated
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "frontend",
|
"name": "frontend",
|
||||||
"version": "0.6.0",
|
"version": "0.6.2",
|
||||||
"description": "",
|
"description": "",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@ -41,7 +41,7 @@
|
|||||||
"eslint-plugin-react": "^7.34.0",
|
"eslint-plugin-react": "^7.34.0",
|
||||||
"eslint-plugin-react-hooks": "^4.6.0",
|
"eslint-plugin-react-hooks": "^4.6.0",
|
||||||
"jest": "^29.6.1",
|
"jest": "^29.6.1",
|
||||||
"prettier": "^3.2.5",
|
"prettier": "^3.3.2",
|
||||||
"ts-node": "^10.9.2",
|
"ts-node": "^10.9.2",
|
||||||
"typescript": "^5.4.2",
|
"typescript": "^5.4.2",
|
||||||
"webpack": "^5.89.0",
|
"webpack": "^5.89.0",
|
||||||
@ -57,10 +57,10 @@
|
|||||||
"@mui/lab": "^5.0.0-alpha.136",
|
"@mui/lab": "^5.0.0-alpha.136",
|
||||||
"@mui/material": "^5.15.14",
|
"@mui/material": "^5.15.14",
|
||||||
"@mui/system": "^5.15.11",
|
"@mui/system": "^5.15.11",
|
||||||
"@mui/x-data-grid": "^7.3.0",
|
"@mui/x-data-grid": "^7.6.0",
|
||||||
"@mui/x-date-pickers": "^7.2.0",
|
"@mui/x-date-pickers": "^7.2.0",
|
||||||
"@nivo/core": "^0.85.1",
|
"@nivo/core": "^0.86.0",
|
||||||
"@nivo/line": "^0.85.1",
|
"@nivo/line": "^0.86.0",
|
||||||
"base-ex": "^0.8.1",
|
"base-ex": "^0.8.1",
|
||||||
"country-flag-icons": "^1.5.11",
|
"country-flag-icons": "^1.5.11",
|
||||||
"date-fns": "^2.30.0",
|
"date-fns": "^2.30.0",
|
||||||
|
@ -178,7 +178,7 @@ const Onboarding = ({
|
|||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
{slot?.hashId ? (
|
{slot?.nickname ? (
|
||||||
<Grid item>
|
<Grid item>
|
||||||
<Typography align='center'>{t('Hi! My name is')}</Typography>
|
<Typography align='center'>{t('Hi! My name is')}</Typography>
|
||||||
<Typography component='h5' variant='h5'>
|
<Typography component='h5' variant='h5'>
|
||||||
|
@ -90,7 +90,7 @@ const RobotProfile = ({
|
|||||||
sx={{ width: '100%' }}
|
sx={{ width: '100%' }}
|
||||||
>
|
>
|
||||||
<Grid item sx={{ height: '2.3em', position: 'relative' }}>
|
<Grid item sx={{ height: '2.3em', position: 'relative' }}>
|
||||||
{slot?.hashId ? (
|
{slot?.nickname ? (
|
||||||
<Typography align='center' component='h5' variant='h5'>
|
<Typography align='center' component='h5' variant='h5'>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
|
@ -44,7 +44,7 @@ const RobotPage = (): JSX.Element => {
|
|||||||
const token = urlToken ?? garage.currentSlot;
|
const token = urlToken ?? garage.currentSlot;
|
||||||
if (token !== undefined && token !== null && page === 'robot') {
|
if (token !== undefined && token !== null && page === 'robot') {
|
||||||
setInputToken(token);
|
setInputToken(token);
|
||||||
if (window.NativeRobosats === undefined || torStatus === '"Done"') {
|
if (window.NativeRobosats === undefined || torStatus === 'ON' || !settings.useProxy) {
|
||||||
getGenerateRobot(token);
|
getGenerateRobot(token);
|
||||||
setView('profile');
|
setView('profile');
|
||||||
}
|
}
|
||||||
@ -83,7 +83,7 @@ const RobotPage = (): JSX.Element => {
|
|||||||
garage.deleteSlot();
|
garage.deleteSlot();
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!(window.NativeRobosats === undefined) && !(torStatus === 'DONE' || torStatus === '"Done"')) {
|
if (settings.useProxy && !(window.NativeRobosats === undefined) && !(torStatus === 'ON')) {
|
||||||
return (
|
return (
|
||||||
<Paper
|
<Paper
|
||||||
elevation={12}
|
elevation={12}
|
||||||
|
@ -45,7 +45,6 @@ const ClickThroughDataGrid = styled(DataGrid)({
|
|||||||
'& .MuiDataGrid-overlayWrapperInner': {
|
'& .MuiDataGrid-overlayWrapperInner': {
|
||||||
pointerEvents: 'none',
|
pointerEvents: 'none',
|
||||||
},
|
},
|
||||||
...{ headerStyleFix },
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const premiumColor = function (baseColor: string, accentColor: string, point: number): string {
|
const premiumColor = function (baseColor: string, accentColor: string, point: number): string {
|
||||||
@ -897,6 +896,11 @@ const BookTable = ({
|
|||||||
: orders;
|
: orders;
|
||||||
}, [showControls, orders, fav, paymentMethods]);
|
}, [showControls, orders, fav, paymentMethods]);
|
||||||
|
|
||||||
|
const loadingPercentage =
|
||||||
|
((federation.exchange.enabledCoordinators - federation.exchange.loadingCoordinators) /
|
||||||
|
federation.exchange.enabledCoordinators) *
|
||||||
|
100;
|
||||||
|
|
||||||
if (!fullscreen) {
|
if (!fullscreen) {
|
||||||
return (
|
return (
|
||||||
<Paper
|
<Paper
|
||||||
@ -908,6 +912,7 @@ const BookTable = ({
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<ClickThroughDataGrid
|
<ClickThroughDataGrid
|
||||||
|
sx={headerStyleFix}
|
||||||
localeText={localeText}
|
localeText={localeText}
|
||||||
rowHeight={3.714 * theme.typography.fontSize}
|
rowHeight={3.714 * theme.typography.fontSize}
|
||||||
headerHeight={3.25 * theme.typography.fontSize}
|
headerHeight={3.25 * theme.typography.fontSize}
|
||||||
@ -928,12 +933,8 @@ const BookTable = ({
|
|||||||
setPaymentMethods,
|
setPaymentMethods,
|
||||||
},
|
},
|
||||||
loadingOverlay: {
|
loadingOverlay: {
|
||||||
variant: 'determinate',
|
variant: loadingPercentage === 0 ? 'indeterminate' : 'determinate',
|
||||||
value:
|
value: loadingPercentage,
|
||||||
((federation.exchange.enabledCoordinators -
|
|
||||||
federation.exchange.loadingCoordinators) /
|
|
||||||
federation.exchange.enabledCoordinators) *
|
|
||||||
100,
|
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
paginationModel={paginationModel}
|
paginationModel={paginationModel}
|
||||||
@ -949,6 +950,7 @@ const BookTable = ({
|
|||||||
<Dialog open={fullscreen} fullScreen={true}>
|
<Dialog open={fullscreen} fullScreen={true}>
|
||||||
<Paper style={{ width: '100%', height: '100%', overflow: 'auto' }}>
|
<Paper style={{ width: '100%', height: '100%', overflow: 'auto' }}>
|
||||||
<ClickThroughDataGrid
|
<ClickThroughDataGrid
|
||||||
|
sx={headerStyleFix}
|
||||||
localeText={localeText}
|
localeText={localeText}
|
||||||
rowHeight={3.714 * theme.typography.fontSize}
|
rowHeight={3.714 * theme.typography.fontSize}
|
||||||
headerHeight={3.25 * theme.typography.fontSize}
|
headerHeight={3.25 * theme.typography.fontSize}
|
||||||
|
@ -64,7 +64,7 @@ const ProfileDialog = ({ open = false, onClose }: Props): JSX.Element => {
|
|||||||
<ListItem className='profileNickname'>
|
<ListItem className='profileNickname'>
|
||||||
<ListItemText>
|
<ListItemText>
|
||||||
<Typography component='h6' variant='h6'>
|
<Typography component='h6' variant='h6'>
|
||||||
{garage.getSlot()?.nickname !== undefined && (
|
{!garage.getSlot()?.nickname && (
|
||||||
<div style={{ position: 'relative', left: '-7px' }}>
|
<div style={{ position: 'relative', left: '-7px' }}>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import useAutocomplete from '@mui/base/useAutocomplete';
|
import { useAutocomplete } from '@mui/base/useAutocomplete';
|
||||||
import { styled } from '@mui/material/styles';
|
import { styled } from '@mui/material/styles';
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
|
@ -90,12 +90,6 @@ const MakerForm = ({
|
|||||||
const minRangeAmountMultiple = 1.6;
|
const minRangeAmountMultiple = 1.6;
|
||||||
const amountSafeThresholds = [1.03, 0.98];
|
const amountSafeThresholds = [1.03, 0.98];
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
// Why?
|
|
||||||
// const slot = garage.getSlot();
|
|
||||||
// if (slot?.token) void federation.fetchRobot(garage, slot?.token);
|
|
||||||
}, [garage.currentSlot]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setCurrencyCode(currencyDict[fav.currency === 0 ? 1 : fav.currency]);
|
setCurrencyCode(currencyDict[fav.currency === 0 ? 1 : fav.currency]);
|
||||||
}, [coordinatorUpdatedAt]);
|
}, [coordinatorUpdatedAt]);
|
||||||
|
@ -178,7 +178,7 @@ const OrderDetails = ({
|
|||||||
: coordinator.info?.taker_fee ?? 0;
|
: coordinator.info?.taker_fee ?? 0;
|
||||||
const defaultRoutingBudget = 0.001;
|
const defaultRoutingBudget = 0.001;
|
||||||
const btc_now = order.satoshis_now / 100000000;
|
const btc_now = order.satoshis_now / 100000000;
|
||||||
const rate = order.amount > 0 ? order.amount / btc_now : Number(order.max_amount) / btc_now;
|
const rate = Number(order.max_amount ?? order.amount) / btc_now;
|
||||||
|
|
||||||
if (isBuyer) {
|
if (isBuyer) {
|
||||||
if (order.amount > 0) {
|
if (order.amount > 0) {
|
||||||
@ -207,7 +207,7 @@ const OrderDetails = ({
|
|||||||
amount: amountString,
|
amount: amountString,
|
||||||
method: order.payment_method,
|
method: order.payment_method,
|
||||||
});
|
});
|
||||||
receive = t('You receive via Lightning {{amount}} Sats (Approx)', {
|
receive = t('You receive {{amount}} Sats (Approx)', {
|
||||||
amount: sats,
|
amount: sats,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
@ -3,8 +3,8 @@ import SmoothImage from 'react-smooth-image';
|
|||||||
import { Avatar, Badge, Tooltip } from '@mui/material';
|
import { Avatar, Badge, Tooltip } from '@mui/material';
|
||||||
import { SendReceiveIcon } from '../Icons';
|
import { SendReceiveIcon } from '../Icons';
|
||||||
import placeholder from './placeholder.json';
|
import placeholder from './placeholder.json';
|
||||||
import { robohash } from './RobohashGenerator';
|
|
||||||
import { AppContext, type UseAppStoreType } from '../../contexts/AppContext';
|
import { AppContext, type UseAppStoreType } from '../../contexts/AppContext';
|
||||||
|
import { roboidentitiesClient } from '../../services/Roboidentities/Web';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
shortAlias?: string | undefined;
|
shortAlias?: string | undefined;
|
||||||
@ -54,10 +54,9 @@ const RobotAvatar: React.FC<Props> = ({
|
|||||||
const className = placeholderType === 'loading' ? 'loadingAvatar' : 'generatingAvatar';
|
const className = placeholderType === 'loading' ? 'loadingAvatar' : 'generatingAvatar';
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// TODO: HANDLE ANDROID AVATARS TOO (when window.NativeRobosats !== undefined)
|
|
||||||
if (hashId !== undefined) {
|
if (hashId !== undefined) {
|
||||||
robohash
|
roboidentitiesClient
|
||||||
.generate(hashId, small ? 'small' : 'large')
|
.generateRobohash(hashId, small ? 'small' : 'large')
|
||||||
.then((avatar) => {
|
.then((avatar) => {
|
||||||
setAvatarSrc(avatar);
|
setAvatarSrc(avatar);
|
||||||
})
|
})
|
||||||
@ -78,9 +77,7 @@ const RobotAvatar: React.FC<Props> = ({
|
|||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
setAvatarSrc(
|
setAvatarSrc(
|
||||||
`file:///android_asset/Web.bundle/assets/federation/avatars/${shortAlias}${
|
`file:///android_asset/Web.bundle/assets/federation/avatars/${shortAlias}.webp`,
|
||||||
small ? ' .small' : ''
|
|
||||||
}.webp`,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
@ -95,7 +95,6 @@ const RobotInfo: React.FC<Props> = ({ coordinator, onClose, disabled }: Props) =
|
|||||||
(signedInvoice) => {
|
(signedInvoice) => {
|
||||||
console.log('Signed message:', signedInvoice);
|
console.log('Signed message:', signedInvoice);
|
||||||
void coordinator.fetchReward(signedInvoice, garage, slot?.token).then((data) => {
|
void coordinator.fetchReward(signedInvoice, garage, slot?.token).then((data) => {
|
||||||
console.log(data);
|
|
||||||
setBadInvoice(data.bad_invoice ?? '');
|
setBadInvoice(data.bad_invoice ?? '');
|
||||||
setShowRewardsSpinner(false);
|
setShowRewardsSpinner(false);
|
||||||
setWithdrawn(data.successful_withdrawal);
|
setWithdrawn(data.successful_withdrawal);
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import React, { useContext } from 'react';
|
import React, { useContext, useEffect } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { type UseAppStoreType, AppContext } from '../../contexts/AppContext';
|
import { type UseAppStoreType, AppContext } from '../../contexts/AppContext';
|
||||||
import {
|
import {
|
||||||
@ -28,17 +28,19 @@ import {
|
|||||||
QrCode,
|
QrCode,
|
||||||
} from '@mui/icons-material';
|
} from '@mui/icons-material';
|
||||||
import { systemClient } from '../../services/System';
|
import { systemClient } from '../../services/System';
|
||||||
|
import { TorIcon } from '../Icons';
|
||||||
import SwapCalls from '@mui/icons-material/SwapCalls';
|
import SwapCalls from '@mui/icons-material/SwapCalls';
|
||||||
import { FederationContext, type UseFederationStoreType } from '../../contexts/FederationContext';
|
import { FederationContext, type UseFederationStoreType } from '../../contexts/FederationContext';
|
||||||
|
import { GarageContext, UseGarageStoreType } from '../../contexts/GarageContext';
|
||||||
|
|
||||||
interface SettingsFormProps {
|
interface SettingsFormProps {
|
||||||
dense?: boolean;
|
dense?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const SettingsForm = ({ dense = false }: SettingsFormProps): JSX.Element => {
|
const SettingsForm = ({ dense = false }: SettingsFormProps): JSX.Element => {
|
||||||
const { fav, setFav, origin, hostUrl, settings, setSettings } =
|
const { fav, setFav, settings, setSettings } = useContext<UseAppStoreType>(AppContext);
|
||||||
useContext<UseAppStoreType>(AppContext);
|
|
||||||
const { federation } = useContext<UseFederationStoreType>(FederationContext);
|
const { federation } = useContext<UseFederationStoreType>(FederationContext);
|
||||||
|
const { garage } = useContext<UseGarageStoreType>(GarageContext);
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const fontSizes = [
|
const fontSizes = [
|
||||||
@ -226,7 +228,6 @@ const SettingsForm = ({ dense = false }: SettingsFormProps): JSX.Element => {
|
|||||||
value={settings.network}
|
value={settings.network}
|
||||||
onChange={(e, network) => {
|
onChange={(e, network) => {
|
||||||
setSettings({ ...settings, network });
|
setSettings({ ...settings, network });
|
||||||
void federation.updateUrls(origin, { ...settings, network }, hostUrl);
|
|
||||||
systemClient.setItem('settings_network', network);
|
systemClient.setItem('settings_network', network);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@ -238,6 +239,29 @@ const SettingsForm = ({ dense = false }: SettingsFormProps): JSX.Element => {
|
|||||||
</ToggleButton>
|
</ToggleButton>
|
||||||
</ToggleButtonGroup>
|
</ToggleButtonGroup>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
|
|
||||||
|
{window.NativeRobosats !== undefined && (
|
||||||
|
<ListItem>
|
||||||
|
<ListItemIcon>
|
||||||
|
<TorIcon />
|
||||||
|
</ListItemIcon>
|
||||||
|
<ToggleButtonGroup
|
||||||
|
exclusive={true}
|
||||||
|
value={settings.useProxy}
|
||||||
|
onChange={(_e, useProxy) => {
|
||||||
|
setSettings({ ...settings, useProxy });
|
||||||
|
systemClient.setItem('settings_use_proxy', String(useProxy));
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ToggleButton value={true} color='primary'>
|
||||||
|
{t('Build-in')}
|
||||||
|
</ToggleButton>
|
||||||
|
<ToggleButton value={false} color='secondary'>
|
||||||
|
{t('Disabled')}
|
||||||
|
</ToggleButton>
|
||||||
|
</ToggleButtonGroup>
|
||||||
|
</ListItem>
|
||||||
|
)}
|
||||||
</List>
|
</List>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
@ -55,14 +55,14 @@ const TorIndicator = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const TorConnectionBadge = (): JSX.Element => {
|
const TorConnectionBadge = (): JSX.Element => {
|
||||||
const { torStatus } = useContext<UseAppStoreType>(AppContext);
|
const { torStatus, settings } = useContext<UseAppStoreType>(AppContext);
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
if (window?.NativeRobosats == null) {
|
if (window?.NativeRobosats == null || !settings.useProxy) {
|
||||||
return <></>;
|
return <></>;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (torStatus === 'NOTINIT') {
|
if (torStatus === 'OFF' || torStatus === 'STOPPING') {
|
||||||
return (
|
return (
|
||||||
<TorIndicator
|
<TorIndicator
|
||||||
color='primary'
|
color='primary'
|
||||||
@ -80,7 +80,7 @@ const TorConnectionBadge = (): JSX.Element => {
|
|||||||
title={t('Connecting to TOR network')}
|
title={t('Connecting to TOR network')}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
} else if (torStatus === '"Done"' || torStatus === 'DONE') {
|
} else if (torStatus === 'ON') {
|
||||||
return <TorIndicator color='success' progress={false} title={t('Connected to TOR network')} />;
|
return <TorIndicator color='success' progress={false} title={t('Connected to TOR network')} />;
|
||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
|
@ -62,7 +62,7 @@ const TorConnectionBadge = (): JSX.Element => {
|
|||||||
return <></>;
|
return <></>;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (torStatus === 'NOTINIT') {
|
if (torStatus === 'OFF' || torStatus === 'STOPING') {
|
||||||
return (
|
return (
|
||||||
<TorIndicator
|
<TorIndicator
|
||||||
color='primary'
|
color='primary'
|
||||||
@ -80,7 +80,7 @@ const TorConnectionBadge = (): JSX.Element => {
|
|||||||
title={t('Connecting to TOR network')}
|
title={t('Connecting to TOR network')}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
} else if (torStatus === '"Done"' || torStatus === 'DONE') {
|
} else if (torStatus === 'ON') {
|
||||||
return <TorIndicator color='success' progress={false} title={t('Connected to TOR network')} />;
|
return <TorIndicator color='success' progress={false} title={t('Connected to TOR network')} />;
|
||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
|
@ -222,6 +222,7 @@ export const SuccessfulPrompt = ({
|
|||||||
takerSummary={order.taker_summary}
|
takerSummary={order.taker_summary}
|
||||||
platformSummary={order.platform_summary}
|
platformSummary={order.platform_summary}
|
||||||
orderId={order.id}
|
orderId={order.id}
|
||||||
|
coordinatorLongAlias={federation.getCoordinator(order.shortAlias)?.longAlias}
|
||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
) : (
|
) : (
|
||||||
|
@ -43,6 +43,7 @@ interface Props {
|
|||||||
makerHashId: string;
|
makerHashId: string;
|
||||||
takerHashId: string;
|
takerHashId: string;
|
||||||
currencyCode: string;
|
currencyCode: string;
|
||||||
|
coordinatorLongAlias: string;
|
||||||
makerSummary: TradeRobotSummary;
|
makerSummary: TradeRobotSummary;
|
||||||
takerSummary: TradeRobotSummary;
|
takerSummary: TradeRobotSummary;
|
||||||
platformSummary: TradeCoordinatorSummary;
|
platformSummary: TradeCoordinatorSummary;
|
||||||
@ -54,6 +55,7 @@ const TradeSummary = ({
|
|||||||
makerHashId,
|
makerHashId,
|
||||||
takerHashId,
|
takerHashId,
|
||||||
currencyCode,
|
currencyCode,
|
||||||
|
coordinatorLongAlias,
|
||||||
makerSummary,
|
makerSummary,
|
||||||
takerSummary,
|
takerSummary,
|
||||||
platformSummary,
|
platformSummary,
|
||||||
@ -72,6 +74,7 @@ const TradeSummary = ({
|
|||||||
|
|
||||||
const onClickExport = function (): void {
|
const onClickExport = function (): void {
|
||||||
const summary = {
|
const summary = {
|
||||||
|
coordinator: coordinatorLongAlias,
|
||||||
order_id: orderId,
|
order_id: orderId,
|
||||||
currency: currencyCode,
|
currency: currencyCode,
|
||||||
maker: makerSummary,
|
maker: makerSummary,
|
||||||
|
@ -37,7 +37,7 @@ export interface SlideDirection {
|
|||||||
out: 'left' | 'right' | undefined;
|
out: 'left' | 'right' | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TorStatus = 'NOTINIT' | 'STARTING' | '"Done"' | 'DONE';
|
export type TorStatus = 'ON' | 'STARTING' | 'STOPPING' | 'OFF';
|
||||||
|
|
||||||
export const isNativeRoboSats = !(window.NativeRobosats === undefined);
|
export const isNativeRoboSats = !(window.NativeRobosats === undefined);
|
||||||
|
|
||||||
@ -155,8 +155,8 @@ export interface UseAppStoreType {
|
|||||||
|
|
||||||
export const initialAppContext: UseAppStoreType = {
|
export const initialAppContext: UseAppStoreType = {
|
||||||
theme: undefined,
|
theme: undefined,
|
||||||
torStatus: 'NOTINIT',
|
torStatus: 'STARTING',
|
||||||
settings: getSettings(),
|
settings: new Settings(),
|
||||||
setSettings: () => {},
|
setSettings: () => {},
|
||||||
page: entryPage,
|
page: entryPage,
|
||||||
setPage: () => {},
|
setPage: () => {},
|
||||||
@ -225,7 +225,7 @@ export const AppContextProvider = ({ children }: AppContextProviderProps): JSX.E
|
|||||||
() => {
|
() => {
|
||||||
setTorStatus(event?.detail);
|
setTorStatus(event?.detail);
|
||||||
},
|
},
|
||||||
event?.detail === '"Done"' ? 5000 : 0,
|
event?.detail === 'ON' ? 5000 : 0,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
@ -9,12 +9,13 @@ import React, {
|
|||||||
type ReactNode,
|
type ReactNode,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
|
|
||||||
import { type Order, Federation } from '../models';
|
import { type Order, Federation, Settings } from '../models';
|
||||||
|
|
||||||
import { federationLottery } from '../utils';
|
import { federationLottery } from '../utils';
|
||||||
|
|
||||||
import { AppContext, type UseAppStoreType } from './AppContext';
|
import { AppContext, type UseAppStoreType } from './AppContext';
|
||||||
import { GarageContext, type UseGarageStoreType } from './GarageContext';
|
import { GarageContext, type UseGarageStoreType } from './GarageContext';
|
||||||
|
import NativeRobosats from '../services/Native';
|
||||||
|
|
||||||
// Refresh delays (ms) according to Order status
|
// Refresh delays (ms) according to Order status
|
||||||
const defaultDelay = 5000;
|
const defaultDelay = 5000;
|
||||||
@ -61,7 +62,7 @@ export interface UseFederationStoreType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const initialFederationContext: UseFederationStoreType = {
|
export const initialFederationContext: UseFederationStoreType = {
|
||||||
federation: new Federation(),
|
federation: new Federation('onion', new Settings(), ''),
|
||||||
sortedCoordinators: [],
|
sortedCoordinators: [],
|
||||||
setDelay: () => {},
|
setDelay: () => {},
|
||||||
currentOrderId: { id: null, shortAlias: null },
|
currentOrderId: { id: null, shortAlias: null },
|
||||||
@ -79,7 +80,7 @@ export const FederationContextProvider = ({
|
|||||||
const { settings, page, origin, hostUrl, open, torStatus } =
|
const { settings, page, origin, hostUrl, open, torStatus } =
|
||||||
useContext<UseAppStoreType>(AppContext);
|
useContext<UseAppStoreType>(AppContext);
|
||||||
const { setMaker, garage, setBadOrder } = useContext<UseGarageStoreType>(GarageContext);
|
const { setMaker, garage, setBadOrder } = useContext<UseGarageStoreType>(GarageContext);
|
||||||
const [federation, setFederation] = useState(initialFederationContext.federation);
|
const [federation] = useState(new Federation(origin, settings, hostUrl));
|
||||||
const sortedCoordinators = useMemo(() => federationLottery(federation), []);
|
const sortedCoordinators = useMemo(() => federationLottery(federation), []);
|
||||||
const [coordinatorUpdatedAt, setCoordinatorUpdatedAt] = useState<string>(
|
const [coordinatorUpdatedAt, setCoordinatorUpdatedAt] = useState<string>(
|
||||||
new Date().toISOString(),
|
new Date().toISOString(),
|
||||||
@ -101,20 +102,23 @@ export const FederationContextProvider = ({
|
|||||||
setMaker((maker) => {
|
setMaker((maker) => {
|
||||||
return { ...maker, coordinator: sortedCoordinators[0] };
|
return { ...maker, coordinator: sortedCoordinators[0] };
|
||||||
}); // default MakerForm coordinator is decided via sorted lottery
|
}); // default MakerForm coordinator is decided via sorted lottery
|
||||||
|
federation.registerHook('onFederationUpdate', () => {
|
||||||
|
setFederationUpdatedAt(new Date().toISOString());
|
||||||
|
});
|
||||||
|
federation.registerHook('onCoordinatorUpdate', () => {
|
||||||
|
setCoordinatorUpdatedAt(new Date().toISOString());
|
||||||
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// On bitcoin network change we reset book, limits and federation info and fetch everything again
|
if (window.NativeRobosats === undefined || torStatus === 'ON' || !settings.useProxy) {
|
||||||
const newFed = initialFederationContext.federation;
|
void federation.updateUrl(origin, settings, hostUrl);
|
||||||
newFed.registerHook('onFederationUpdate', () => {
|
void federation.update();
|
||||||
setFederationUpdatedAt(new Date().toISOString());
|
|
||||||
});
|
const token = garage.getSlot()?.getRobot()?.token;
|
||||||
newFed.registerHook('onCoordinatorUpdate', () => {
|
if (token) void federation.fetchRobot(garage, token);
|
||||||
setCoordinatorUpdatedAt(new Date().toISOString());
|
}
|
||||||
});
|
}, [settings.network, settings.useProxy, torStatus]);
|
||||||
void newFed.start(origin, settings, hostUrl);
|
|
||||||
setFederation(newFed);
|
|
||||||
}, [settings.network, torStatus]);
|
|
||||||
|
|
||||||
const onOrderReceived = (order: Order): void => {
|
const onOrderReceived = (order: Order): void => {
|
||||||
let newDelay = defaultDelay;
|
let newDelay = defaultDelay;
|
||||||
@ -176,15 +180,6 @@ export const FederationContextProvider = ({
|
|||||||
if (page === 'offers') void federation.updateBook();
|
if (page === 'offers') void federation.updateBook();
|
||||||
}, [page]);
|
}, [page]);
|
||||||
|
|
||||||
// use effects to fetchRobots on app start and network change
|
|
||||||
useEffect(() => {
|
|
||||||
const slot = garage.getSlot();
|
|
||||||
const robot = slot?.getRobot();
|
|
||||||
|
|
||||||
if (robot && garage.currentSlot && slot?.token && robot.encPrivKey && robot.pubKey) {
|
|
||||||
void federation.fetchRobot(garage, slot.token);
|
|
||||||
}
|
|
||||||
}, [settings.network]);
|
|
||||||
// use effects to fetchRobots on Profile open
|
// use effects to fetchRobots on Profile open
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const slot = garage.getSlot();
|
const slot = garage.getSlot();
|
||||||
|
@ -6,11 +6,11 @@ import {
|
|||||||
type Order,
|
type Order,
|
||||||
type Garage,
|
type Garage,
|
||||||
} from '.';
|
} from '.';
|
||||||
|
import { roboidentitiesClient } from '../services/Roboidentities/Web';
|
||||||
import { apiClient } from '../services/api';
|
import { apiClient } from '../services/api';
|
||||||
import { validateTokenEntropy } from '../utils';
|
import { validateTokenEntropy } from '../utils';
|
||||||
import { compareUpdateLimit } from './Limit.model';
|
import { compareUpdateLimit } from './Limit.model';
|
||||||
import { defaultOrder } from './Order.model';
|
import { defaultOrder } from './Order.model';
|
||||||
import { robohash } from '../components/RobotAvatar/RobohashGenerator';
|
|
||||||
|
|
||||||
export interface Contact {
|
export interface Contact {
|
||||||
nostr?: string | undefined;
|
nostr?: string | undefined;
|
||||||
@ -97,7 +97,7 @@ function calculateSizeLimit(inputDate: Date): number {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class Coordinator {
|
export class Coordinator {
|
||||||
constructor(value: any) {
|
constructor(value: any, origin: Origin, settings: Settings, hostUrl: string) {
|
||||||
const established = new Date(value.established);
|
const established = new Date(value.established);
|
||||||
this.longAlias = value.longAlias;
|
this.longAlias = value.longAlias;
|
||||||
this.shortAlias = value.shortAlias;
|
this.shortAlias = value.shortAlias;
|
||||||
@ -115,6 +115,8 @@ export class Coordinator {
|
|||||||
this.testnetNodesPubkeys = value.testnetNodesPubkeys;
|
this.testnetNodesPubkeys = value.testnetNodesPubkeys;
|
||||||
this.url = '';
|
this.url = '';
|
||||||
this.basePath = '';
|
this.basePath = '';
|
||||||
|
|
||||||
|
this.updateUrl(origin, settings, hostUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
// These properties are loaded from federation.json
|
// These properties are loaded from federation.json
|
||||||
@ -143,24 +145,9 @@ export class Coordinator {
|
|||||||
public loadingInfo: boolean = false;
|
public loadingInfo: boolean = false;
|
||||||
public limits: LimitList = {};
|
public limits: LimitList = {};
|
||||||
public loadingLimits: boolean = false;
|
public loadingLimits: boolean = false;
|
||||||
public loadingRobot: boolean = true;
|
public loadingRobot: string | null;
|
||||||
|
|
||||||
start = async (
|
updateUrl = (origin: Origin, settings: Settings, hostUrl: string): void => {
|
||||||
origin: Origin,
|
|
||||||
settings: Settings,
|
|
||||||
hostUrl: string,
|
|
||||||
onUpdate: (shortAlias: string) => void = () => {},
|
|
||||||
): Promise<void> => {
|
|
||||||
if (this.enabled !== true) return;
|
|
||||||
void this.updateUrl(settings, origin, hostUrl, onUpdate);
|
|
||||||
};
|
|
||||||
|
|
||||||
updateUrl = async (
|
|
||||||
settings: Settings,
|
|
||||||
origin: Origin,
|
|
||||||
hostUrl: string,
|
|
||||||
onUpdate: (shortAlias: string) => void = () => {},
|
|
||||||
): Promise<void> => {
|
|
||||||
if (settings.selfhostedClient && this.shortAlias !== 'local') {
|
if (settings.selfhostedClient && this.shortAlias !== 'local') {
|
||||||
this.url = hostUrl;
|
this.url = hostUrl;
|
||||||
this.basePath = `/${settings.network}/${this.shortAlias}`;
|
this.basePath = `/${settings.network}/${this.shortAlias}`;
|
||||||
@ -168,9 +155,6 @@ export class Coordinator {
|
|||||||
this.url = String(this[settings.network][origin]);
|
this.url = String(this[settings.network][origin]);
|
||||||
this.basePath = '';
|
this.basePath = '';
|
||||||
}
|
}
|
||||||
void this.update(() => {
|
|
||||||
onUpdate(this.shortAlias);
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
update = async (onUpdate: (shortAlias: string) => void = () => {}): Promise<void> => {
|
update = async (onUpdate: (shortAlias: string) => void = () => {}): Promise<void> => {
|
||||||
@ -191,7 +175,7 @@ export class Coordinator {
|
|||||||
|
|
||||||
generateAllMakerAvatars = async (data: [PublicOrder]): Promise<void> => {
|
generateAllMakerAvatars = async (data: [PublicOrder]): Promise<void> => {
|
||||||
for (const order of data) {
|
for (const order of data) {
|
||||||
void robohash.generate(order.maker_hash_id, 'small');
|
roboidentitiesClient.generateRobohash(order.maker_hash_id, 'small');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -201,6 +185,7 @@ export class Coordinator {
|
|||||||
if (this.loadingBook) return;
|
if (this.loadingBook) return;
|
||||||
|
|
||||||
this.loadingBook = true;
|
this.loadingBook = true;
|
||||||
|
this.book = [];
|
||||||
|
|
||||||
apiClient
|
apiClient
|
||||||
.get(this.url, `${this.basePath}/api/book/`)
|
.get(this.url, `${this.basePath}/api/book/`)
|
||||||
@ -313,7 +298,7 @@ export class Coordinator {
|
|||||||
};
|
};
|
||||||
|
|
||||||
fetchRobot = async (garage: Garage, token: string): Promise<Robot | null> => {
|
fetchRobot = async (garage: Garage, token: string): Promise<Robot | null> => {
|
||||||
if (!this.enabled || !token) return null;
|
if (!this.enabled || !token || this.loadingRobot === token) return null;
|
||||||
|
|
||||||
const robot = garage?.getSlot(token)?.getRobot() ?? null;
|
const robot = garage?.getSlot(token)?.getRobot() ?? null;
|
||||||
const authHeaders = robot?.getAuthHeaders();
|
const authHeaders = robot?.getAuthHeaders();
|
||||||
@ -324,6 +309,8 @@ export class Coordinator {
|
|||||||
|
|
||||||
if (!hasEnoughEntropy) return null;
|
if (!hasEnoughEntropy) return null;
|
||||||
|
|
||||||
|
this.loadingRobot = token;
|
||||||
|
|
||||||
garage.updateRobot(token, this.shortAlias, { loading: true });
|
garage.updateRobot(token, this.shortAlias, { loading: true });
|
||||||
|
|
||||||
const newAttributes = await apiClient
|
const newAttributes = await apiClient
|
||||||
@ -346,7 +333,8 @@ export class Coordinator {
|
|||||||
})
|
})
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
console.log(e);
|
console.log(e);
|
||||||
});
|
})
|
||||||
|
.finally(() => (this.loadingRobot = null));
|
||||||
|
|
||||||
garage.updateRobot(token, this.shortAlias, {
|
garage.updateRobot(token, this.shortAlias, {
|
||||||
...newAttributes,
|
...newAttributes,
|
||||||
@ -370,7 +358,6 @@ export class Coordinator {
|
|||||||
return await apiClient
|
return await apiClient
|
||||||
.get(this.url, `${this.basePath}/api/order/?order_id=${orderId}`, authHeaders)
|
.get(this.url, `${this.basePath}/api/order/?order_id=${orderId}`, authHeaders)
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
console.log('data', data);
|
|
||||||
const order: Order = {
|
const order: Order = {
|
||||||
...defaultOrder,
|
...defaultOrder,
|
||||||
...data,
|
...data,
|
||||||
|
@ -14,14 +14,14 @@ import { updateExchangeInfo } from './Exchange.model';
|
|||||||
type FederationHooks = 'onCoordinatorUpdate' | 'onFederationUpdate';
|
type FederationHooks = 'onCoordinatorUpdate' | 'onFederationUpdate';
|
||||||
|
|
||||||
export class Federation {
|
export class Federation {
|
||||||
constructor() {
|
constructor(origin: Origin, settings: Settings, hostUrl: string) {
|
||||||
this.coordinators = Object.entries(defaultFederation).reduce(
|
this.coordinators = Object.entries(defaultFederation).reduce(
|
||||||
(acc: Record<string, Coordinator>, [key, value]: [string, any]) => {
|
(acc: Record<string, Coordinator>, [key, value]: [string, any]) => {
|
||||||
if (getHost() !== '127.0.0.1:8000' && key === 'local') {
|
if (getHost() !== '127.0.0.1:8000' && key === 'local') {
|
||||||
// Do not add `Local Dev` unless it is running on localhost
|
// Do not add `Local Dev` unless it is running on localhost
|
||||||
return acc;
|
return acc;
|
||||||
} else {
|
} else {
|
||||||
acc[key] = new Coordinator(value);
|
acc[key] = new Coordinator(value, origin, settings, hostUrl);
|
||||||
return acc;
|
return acc;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -36,7 +36,16 @@ export class Federation {
|
|||||||
onCoordinatorUpdate: [],
|
onCoordinatorUpdate: [],
|
||||||
onFederationUpdate: [],
|
onFederationUpdate: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
|
this.exchange.loadingCoordinators = Object.keys(this.coordinators).length;
|
||||||
|
|
||||||
|
const host = getHost();
|
||||||
|
const url = `${window.location.protocol}//${host}`;
|
||||||
|
const tesnetHost = Object.values(this.coordinators).find((coor) => {
|
||||||
|
return Object.values(coor.testnet).includes(url);
|
||||||
|
});
|
||||||
|
if (tesnetHost) settings.network = 'testnet';
|
||||||
}
|
}
|
||||||
|
|
||||||
public coordinators: Record<string, Coordinator>;
|
public coordinators: Record<string, Coordinator>;
|
||||||
@ -69,38 +78,10 @@ export class Federation {
|
|||||||
this.triggerHook('onFederationUpdate');
|
this.triggerHook('onFederationUpdate');
|
||||||
};
|
};
|
||||||
|
|
||||||
// Setup
|
updateUrl = async (origin: Origin, settings: Settings, hostUrl: string): Promise<void> => {
|
||||||
start = async (origin: Origin, settings: Settings, hostUrl: string): Promise<void> => {
|
|
||||||
const onCoordinatorStarted = (): void => {
|
|
||||||
this.exchange.onlineCoordinators = this.exchange.onlineCoordinators + 1;
|
|
||||||
this.onCoordinatorSaved();
|
|
||||||
};
|
|
||||||
|
|
||||||
this.loading = true;
|
|
||||||
this.exchange.loadingCoordinators = Object.keys(this.coordinators).length;
|
|
||||||
|
|
||||||
const host = getHost();
|
|
||||||
const url = `${window.location.protocol}//${host}`;
|
|
||||||
const tesnetHost = Object.values(this.coordinators).find((coor) => {
|
|
||||||
return Object.values(coor.testnet).includes(url);
|
|
||||||
});
|
|
||||||
if (tesnetHost) settings.network = 'testnet';
|
|
||||||
|
|
||||||
for (const coor of Object.values(this.coordinators)) {
|
for (const coor of Object.values(this.coordinators)) {
|
||||||
if (coor.enabled) {
|
coor.updateUrl(origin, settings, hostUrl);
|
||||||
await coor.start(origin, settings, hostUrl, onCoordinatorStarted);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
this.updateEnabledCoordinators();
|
|
||||||
};
|
|
||||||
|
|
||||||
// On Testnet/Mainnet change
|
|
||||||
updateUrls = async (origin: Origin, settings: Settings, hostUrl: string): Promise<void> => {
|
|
||||||
this.loading = true;
|
|
||||||
for (const coor of Object.values(this.coordinators)) {
|
|
||||||
await coor.updateUrl(settings, origin, hostUrl);
|
|
||||||
}
|
|
||||||
this.loading = false;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
update = async (): Promise<void> => {
|
update = async (): Promise<void> => {
|
||||||
@ -115,9 +96,12 @@ export class Federation {
|
|||||||
lifetime_volume: 0,
|
lifetime_volume: 0,
|
||||||
version: { major: 0, minor: 0, patch: 0 },
|
version: { major: 0, minor: 0, patch: 0 },
|
||||||
};
|
};
|
||||||
|
this.exchange.onlineCoordinators = 0;
|
||||||
this.exchange.loadingCoordinators = Object.keys(this.coordinators).length;
|
this.exchange.loadingCoordinators = Object.keys(this.coordinators).length;
|
||||||
|
this.updateEnabledCoordinators();
|
||||||
for (const coor of Object.values(this.coordinators)) {
|
for (const coor of Object.values(this.coordinators)) {
|
||||||
await coor.update(() => {
|
coor.update(() => {
|
||||||
|
this.exchange.onlineCoordinators = this.exchange.onlineCoordinators + 1;
|
||||||
this.onCoordinatorSaved();
|
this.onCoordinatorSaved();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -125,10 +109,11 @@ export class Federation {
|
|||||||
|
|
||||||
updateBook = async (): Promise<void> => {
|
updateBook = async (): Promise<void> => {
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
|
this.book = [];
|
||||||
this.triggerHook('onCoordinatorUpdate');
|
this.triggerHook('onCoordinatorUpdate');
|
||||||
this.exchange.loadingCoordinators = Object.keys(this.coordinators).length;
|
this.exchange.loadingCoordinators = Object.keys(this.coordinators).length;
|
||||||
for (const coor of Object.values(this.coordinators)) {
|
for (const coor of Object.values(this.coordinators)) {
|
||||||
await coor.updateBook(() => {
|
coor.updateBook(() => {
|
||||||
this.onCoordinatorSaved();
|
this.onCoordinatorSaved();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -59,7 +59,9 @@ class Garage {
|
|||||||
const rawSlots = JSON.parse(slotsDump);
|
const rawSlots = JSON.parse(slotsDump);
|
||||||
Object.values(rawSlots).forEach((rawSlot: Record<any, any>) => {
|
Object.values(rawSlots).forEach((rawSlot: Record<any, any>) => {
|
||||||
if (rawSlot?.token) {
|
if (rawSlot?.token) {
|
||||||
this.slots[rawSlot.token] = new Slot(rawSlot.token, Object.keys(rawSlot.robots), {});
|
this.slots[rawSlot.token] = new Slot(rawSlot.token, Object.keys(rawSlot.robots), {}, () =>
|
||||||
|
this.triggerHook('onRobotUpdate'),
|
||||||
|
);
|
||||||
|
|
||||||
Object.keys(rawSlot.robots).forEach((shortAlias) => {
|
Object.keys(rawSlot.robots).forEach((shortAlias) => {
|
||||||
const rawRobot = rawSlot.robots[shortAlias];
|
const rawRobot = rawSlot.robots[shortAlias];
|
||||||
@ -113,9 +115,10 @@ class Garage {
|
|||||||
if (!token || !shortAliases) return;
|
if (!token || !shortAliases) return;
|
||||||
|
|
||||||
if (this.getSlot(token) === null) {
|
if (this.getSlot(token) === null) {
|
||||||
this.slots[token] = new Slot(token, shortAliases, attributes);
|
this.slots[token] = new Slot(token, shortAliases, attributes, () =>
|
||||||
|
this.triggerHook('onRobotUpdate'),
|
||||||
|
);
|
||||||
this.save();
|
this.save();
|
||||||
this.triggerHook('onRobotUpdate');
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import i18n from '../i18n/Web';
|
import i18n from '../i18n/Web';
|
||||||
import { systemClient } from '../services/System';
|
import { systemClient } from '../services/System';
|
||||||
|
import { apiClient } from '../services/api';
|
||||||
import { getHost } from '../utils';
|
import { getHost } from '../utils';
|
||||||
|
|
||||||
export type Language =
|
export type Language =
|
||||||
@ -42,8 +43,13 @@ class BaseSettings {
|
|||||||
: i18n.resolvedLanguage.substring(0, 2);
|
: i18n.resolvedLanguage.substring(0, 2);
|
||||||
|
|
||||||
const networkCookie = systemClient.getItem('settings_network');
|
const networkCookie = systemClient.getItem('settings_network');
|
||||||
this.network = networkCookie !== '' ? networkCookie : 'mainnet';
|
this.network = networkCookie && networkCookie !== '' ? networkCookie : 'mainnet';
|
||||||
this.host = getHost();
|
this.host = getHost();
|
||||||
|
|
||||||
|
const useProxy = systemClient.getItem('settings_use_proxy');
|
||||||
|
this.useProxy = window.NativeRobosats !== undefined && useProxy !== 'false';
|
||||||
|
|
||||||
|
apiClient.useProxy = this.useProxy;
|
||||||
}
|
}
|
||||||
|
|
||||||
public frontend: 'basic' | 'pro' = 'basic';
|
public frontend: 'basic' | 'pro' = 'basic';
|
||||||
@ -56,6 +62,7 @@ class BaseSettings {
|
|||||||
public host?: string;
|
public host?: string;
|
||||||
public unsafeClient: boolean = false;
|
public unsafeClient: boolean = false;
|
||||||
public selfhostedClient: boolean = false;
|
public selfhostedClient: boolean = false;
|
||||||
|
public useProxy: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default BaseSettings;
|
export default BaseSettings;
|
||||||
|
@ -1,17 +1,24 @@
|
|||||||
import { sha256 } from 'js-sha256';
|
import { sha256 } from 'js-sha256';
|
||||||
import { Robot, type Order } from '.';
|
import { Robot, type Order } from '.';
|
||||||
import { robohash } from '../components/RobotAvatar/RobohashGenerator';
|
import { roboidentitiesClient } from '../services/Roboidentities/Web';
|
||||||
import { generate_roboname } from 'robo-identities-wasm';
|
|
||||||
|
|
||||||
class Slot {
|
class Slot {
|
||||||
constructor(token: string, shortAliases: string[], robotAttributes: Record<any, any>) {
|
constructor(
|
||||||
|
token: string,
|
||||||
|
shortAliases: string[],
|
||||||
|
robotAttributes: Record<any, any>,
|
||||||
|
onRobotUpdate: () => void,
|
||||||
|
) {
|
||||||
this.token = token;
|
this.token = token;
|
||||||
|
|
||||||
this.hashId = sha256(sha256(this.token));
|
this.hashId = sha256(sha256(this.token));
|
||||||
this.nickname = generate_roboname(this.hashId);
|
this.nickname = null;
|
||||||
// trigger RoboHash avatar generation in webworker and store in RoboHash class cache.
|
roboidentitiesClient.generateRoboname(this.hashId).then((nickname) => {
|
||||||
void robohash.generate(this.hashId, 'small');
|
this.nickname = nickname;
|
||||||
void robohash.generate(this.hashId, 'large');
|
onRobotUpdate();
|
||||||
|
});
|
||||||
|
roboidentitiesClient.generateRobohash(this.hashId, 'small');
|
||||||
|
roboidentitiesClient.generateRobohash(this.hashId, 'large');
|
||||||
|
|
||||||
this.robots = shortAliases.reduce((acc: Record<string, Robot>, shortAlias: string) => {
|
this.robots = shortAliases.reduce((acc: Record<string, Robot>, shortAlias: string) => {
|
||||||
acc[shortAlias] = new Robot(robotAttributes);
|
acc[shortAlias] = new Robot(robotAttributes);
|
||||||
@ -22,6 +29,7 @@ class Slot {
|
|||||||
this.activeShortAlias = null;
|
this.activeShortAlias = null;
|
||||||
this.lastShortAlias = null;
|
this.lastShortAlias = null;
|
||||||
this.copiedToken = false;
|
this.copiedToken = false;
|
||||||
|
onRobotUpdate();
|
||||||
}
|
}
|
||||||
|
|
||||||
token: string | null;
|
token: string | null;
|
||||||
|
16
frontend/src/services/Native/index.d.ts
vendored
@ -15,7 +15,7 @@ export interface ReactNativeWebView {
|
|||||||
export interface NativeWebViewMessageHttp {
|
export interface NativeWebViewMessageHttp {
|
||||||
id?: number;
|
id?: number;
|
||||||
category: 'http';
|
category: 'http';
|
||||||
type: 'post' | 'get' | 'put' | 'delete' | 'xhr';
|
type: 'post' | 'get' | 'put' | 'delete';
|
||||||
path: string;
|
path: string;
|
||||||
baseUrl: string;
|
baseUrl: string;
|
||||||
headers?: object;
|
headers?: object;
|
||||||
@ -30,7 +30,19 @@ export interface NativeWebViewMessageSystem {
|
|||||||
detail?: string;
|
detail?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export declare type NativeWebViewMessage = NativeWebViewMessageHttp | NativeWebViewMessageSystem;
|
export interface NativeWebViewMessageRoboidentities {
|
||||||
|
id?: number;
|
||||||
|
category: 'roboidentities';
|
||||||
|
type: 'roboname' | 'robohash';
|
||||||
|
string?: string;
|
||||||
|
size?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export declare type NativeWebViewMessage =
|
||||||
|
| NativeWebViewMessageHttp
|
||||||
|
| NativeWebViewMessageSystem
|
||||||
|
| NativeWebViewMessageRoboidentities
|
||||||
|
| NA;
|
||||||
|
|
||||||
export interface NativeRobosatsPromise {
|
export interface NativeRobosatsPromise {
|
||||||
resolve: (value: object | PromiseLike<object>) => void;
|
resolve: (value: object | PromiseLike<object>) => void;
|
||||||
|
4
frontend/src/services/Roboidentities/Native.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
import RoboidentitiesClientNativeClient from './RoboidentitiesNativeClient';
|
||||||
|
import { RoboidentitiesClient } from './type';
|
||||||
|
|
||||||
|
export const roboidentitiesClient: RoboidentitiesClient = new RoboidentitiesClientNativeClient();
|
@ -0,0 +1,42 @@
|
|||||||
|
import { type RoboidentitiesClient } from '../type';
|
||||||
|
|
||||||
|
class RoboidentitiesNativeClient implements RoboidentitiesClient {
|
||||||
|
private robonames: Record<string, string> = {};
|
||||||
|
private robohashes: Record<string, string> = {};
|
||||||
|
|
||||||
|
public generateRoboname: (initialString: string) => Promise<string> = async (initialString) => {
|
||||||
|
if (this.robonames[initialString]) {
|
||||||
|
return this.robonames[initialString];
|
||||||
|
} else {
|
||||||
|
const response = await window.NativeRobosats?.postMessage({
|
||||||
|
category: 'roboidentities',
|
||||||
|
type: 'roboname',
|
||||||
|
detail: initialString,
|
||||||
|
});
|
||||||
|
const result = response ? Object.values(response)[0] : '';
|
||||||
|
this.robonames[initialString] = result;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
public generateRobohash: (initialString: string, size: 'small' | 'large') => Promise<string> =
|
||||||
|
async (initialString, size) => {
|
||||||
|
const key = `${initialString};${size === 'small' ? 80 : 256}`;
|
||||||
|
|
||||||
|
if (this.robohashes[key]) {
|
||||||
|
return this.robohashes[key];
|
||||||
|
} else {
|
||||||
|
const response = await window.NativeRobosats?.postMessage({
|
||||||
|
category: 'roboidentities',
|
||||||
|
type: 'robohash',
|
||||||
|
detail: key,
|
||||||
|
});
|
||||||
|
const result = response ? Object.values(response)[0] : '';
|
||||||
|
const image = `data:image/png;base64,${result}`;
|
||||||
|
this.robohashes[key] = image;
|
||||||
|
return image;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default RoboidentitiesNativeClient;
|
@ -81,7 +81,7 @@ class RoboGenerator {
|
|||||||
hash,
|
hash,
|
||||||
size,
|
size,
|
||||||
) => {
|
) => {
|
||||||
const cacheKey = `${size}px;${hash}`;
|
const cacheKey = `${hash};${size}`;
|
||||||
if (this.assetsCache[cacheKey]) {
|
if (this.assetsCache[cacheKey]) {
|
||||||
return this.assetsCache[cacheKey];
|
return this.assetsCache[cacheKey];
|
||||||
} else {
|
} else {
|
@ -0,0 +1,18 @@
|
|||||||
|
import { type RoboidentitiesClient } from '../type';
|
||||||
|
import { generate_roboname } from 'robo-identities-wasm';
|
||||||
|
import { robohash } from './RobohashGenerator';
|
||||||
|
|
||||||
|
class RoboidentitiesClientWebClient implements RoboidentitiesClient {
|
||||||
|
public generateRoboname: (initialString: string) => Promise<string> = async (initialString) => {
|
||||||
|
return new Promise<string>(async (resolve, _reject) => {
|
||||||
|
resolve(generate_roboname(initialString));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
public generateRobohash: (initialString: string, size: 'small' | 'large') => Promise<string> =
|
||||||
|
async (initialString, size) => {
|
||||||
|
return robohash.generate(initialString, size);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default RoboidentitiesClientWebClient;
|
4
frontend/src/services/Roboidentities/Web.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
import RoboidentitiesClientWebClient from './RoboidentitiesWebClient';
|
||||||
|
import { RoboidentitiesClient } from './type';
|
||||||
|
|
||||||
|
export const roboidentitiesClient: RoboidentitiesClient = new RoboidentitiesClientWebClient();
|
4
frontend/src/services/Roboidentities/type.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export interface RoboidentitiesClient {
|
||||||
|
generateRoboname: (initialString: string) => Promise<string>;
|
||||||
|
generateRobohash: (initialString: string, size: 'small' | 'large') => Promise<string>;
|
||||||
|
}
|
@ -28,7 +28,7 @@ class SystemNativeClient implements SystemClient {
|
|||||||
};
|
};
|
||||||
|
|
||||||
public setCookie: (key: string, value: string) => void = (key, value) => {
|
public setCookie: (key: string, value: string) => void = (key, value) => {
|
||||||
delete window.NativeRobosats?.cookies[key];
|
window.NativeRobosats?.loadCookie({ key, value });
|
||||||
void window.NativeRobosats?.postMessage({
|
void window.NativeRobosats?.postMessage({
|
||||||
category: 'system',
|
category: 'system',
|
||||||
type: 'setCookie',
|
type: 'setCookie',
|
||||||
|
@ -1,8 +1,12 @@
|
|||||||
import { type ApiClient, type Auth } from '..';
|
import { type ApiClient, type Auth } from '..';
|
||||||
import { systemClient } from '../../System';
|
import { systemClient } from '../../System';
|
||||||
|
import ApiWebClient from '../ApiWebClient';
|
||||||
|
|
||||||
class ApiNativeClient implements ApiClient {
|
class ApiNativeClient implements ApiClient {
|
||||||
private assetsCache: Record<string, string> = {};
|
public useProxy = true;
|
||||||
|
|
||||||
|
private webClient: ApiClient = new ApiWebClient();
|
||||||
|
|
||||||
private readonly assetsPromises = new Map<string, Promise<string | undefined>>();
|
private readonly assetsPromises = new Map<string, Promise<string | undefined>>();
|
||||||
|
|
||||||
private readonly getHeaders: (auth?: Auth) => HeadersInit = (auth) => {
|
private readonly getHeaders: (auth?: Auth) => HeadersInit = (auth) => {
|
||||||
@ -51,6 +55,7 @@ class ApiNativeClient implements ApiClient {
|
|||||||
|
|
||||||
public delete: (baseUrl: string, path: string, auth?: Auth) => Promise<object | undefined> =
|
public delete: (baseUrl: string, path: string, auth?: Auth) => Promise<object | undefined> =
|
||||||
async (baseUrl, path, auth) => {
|
async (baseUrl, path, auth) => {
|
||||||
|
if (!this.proxy) this.webClient.delete(baseUrl, path, auth);
|
||||||
return await window.NativeRobosats?.postMessage({
|
return await window.NativeRobosats?.postMessage({
|
||||||
category: 'http',
|
category: 'http',
|
||||||
type: 'delete',
|
type: 'delete',
|
||||||
@ -66,6 +71,7 @@ class ApiNativeClient implements ApiClient {
|
|||||||
body: object,
|
body: object,
|
||||||
auth?: Auth,
|
auth?: Auth,
|
||||||
) => Promise<object | undefined> = async (baseUrl, path, body, auth) => {
|
) => Promise<object | undefined> = async (baseUrl, path, body, auth) => {
|
||||||
|
if (!this.proxy) this.webClient.post(baseUrl, path, body, auth);
|
||||||
return await window.NativeRobosats?.postMessage({
|
return await window.NativeRobosats?.postMessage({
|
||||||
category: 'http',
|
category: 'http',
|
||||||
type: 'post',
|
type: 'post',
|
||||||
@ -81,6 +87,7 @@ class ApiNativeClient implements ApiClient {
|
|||||||
path,
|
path,
|
||||||
auth,
|
auth,
|
||||||
) => {
|
) => {
|
||||||
|
if (!this.proxy) this.webClient.get(baseUrl, path, auth);
|
||||||
return await window.NativeRobosats?.postMessage({
|
return await window.NativeRobosats?.postMessage({
|
||||||
category: 'http',
|
category: 'http',
|
||||||
type: 'get',
|
type: 'get',
|
||||||
@ -89,41 +96,6 @@ class ApiNativeClient implements ApiClient {
|
|||||||
headers: this.getHeaders(auth),
|
headers: this.getHeaders(auth),
|
||||||
}).then(this.parseResponse);
|
}).then(this.parseResponse);
|
||||||
};
|
};
|
||||||
|
|
||||||
public fileImageUrl: (baseUrl: string, path: string) => Promise<string | undefined> = async (
|
|
||||||
baseUrl,
|
|
||||||
path,
|
|
||||||
) => {
|
|
||||||
if (path === '') {
|
|
||||||
return await Promise.resolve('');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.assetsCache[path] != null) {
|
|
||||||
return await Promise.resolve(this.assetsCache[path]);
|
|
||||||
} else if (this.assetsPromises.has(path)) {
|
|
||||||
return await this.assetsPromises.get(path);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.assetsPromises.set(
|
|
||||||
path,
|
|
||||||
new Promise<string>((resolve, reject) => {
|
|
||||||
window.NativeRobosats?.postMessage({
|
|
||||||
category: 'http',
|
|
||||||
type: 'xhr',
|
|
||||||
baseUrl,
|
|
||||||
path,
|
|
||||||
})
|
|
||||||
.then((fileB64: { b64Data: string }) => {
|
|
||||||
this.assetsCache[path] = `data:image/png;base64,${fileB64.b64Data}`;
|
|
||||||
this.assetsPromises.delete(path);
|
|
||||||
resolve(this.assetsCache[path]);
|
|
||||||
})
|
|
||||||
.catch(reject);
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
return await this.assetsPromises.get(path);
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ApiNativeClient;
|
export default ApiNativeClient;
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
import { type ApiClient, type Auth } from '..';
|
import { type ApiClient, type Auth } from '..';
|
||||||
|
|
||||||
class ApiWebClient implements ApiClient {
|
class ApiWebClient implements ApiClient {
|
||||||
|
public useProxy = false;
|
||||||
|
|
||||||
private readonly getHeaders: (auth?: Auth) => HeadersInit = (auth) => {
|
private readonly getHeaders: (auth?: Auth) => HeadersInit = (auth) => {
|
||||||
let headers = {
|
let headers = {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
|
@ -7,11 +7,11 @@ export interface Auth {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface ApiClient {
|
export interface ApiClient {
|
||||||
|
useProxy: boolean;
|
||||||
post: (baseUrl: string, path: string, body: object, auth?: Auth) => Promise<object | undefined>;
|
post: (baseUrl: string, path: string, body: object, auth?: Auth) => Promise<object | undefined>;
|
||||||
put: (baseUrl: string, path: string, body: object, auth?: Auth) => Promise<object | undefined>;
|
put: (baseUrl: string, path: string, body: object, auth?: Auth) => Promise<object | undefined>;
|
||||||
get: (baseUrl: string, path: string, auth?: Auth) => Promise<object | undefined>;
|
get: (baseUrl: string, path: string, auth?: Auth) => Promise<object | undefined>;
|
||||||
delete: (baseUrl: string, path: string, auth?: Auth) => Promise<object | undefined>;
|
delete: (baseUrl: string, path: string, auth?: Auth) => Promise<object | undefined>;
|
||||||
fileImageUrl?: (baseUrl: string, path: string) => Promise<string | undefined>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const apiClient: ApiClient =
|
export const apiClient: ApiClient =
|
||||||
|
@ -45,7 +45,6 @@ export default function federationLottery(federation: Federation): string[] {
|
|||||||
// federation[shortAlias] = { badges:{ donatesToDevFund }};
|
// federation[shortAlias] = { badges:{ donatesToDevFund }};
|
||||||
// }
|
// }
|
||||||
|
|
||||||
// console.log(federation)
|
|
||||||
// return federation;
|
// return federation;
|
||||||
// }
|
// }
|
||||||
|
|
||||||
@ -58,5 +57,4 @@ export default function federationLottery(federation: Federation): string[] {
|
|||||||
// results.push(rankedCoordinators);
|
// results.push(rankedCoordinators);
|
||||||
// }
|
// }
|
||||||
|
|
||||||
// console.log(results)
|
|
||||||
// }
|
// }
|
||||||
|
@ -25,9 +25,9 @@
|
|||||||
"hasLargeLimits": true
|
"hasLargeLimits": true
|
||||||
},
|
},
|
||||||
"policies": {
|
"policies": {
|
||||||
"Policy Name 1": "Experimental coordinator used for development. Use at your own risk.",
|
"Experimental": "Experimental coordinator used for development. Use at your own risk.",
|
||||||
"Privacy Policy": "...",
|
"Dispute Policy": "Evidence in Disputes: In the event of a dispute, users will be asked to provide transaction-related evidence. This could include transaction IDs, screenshots of payment confirmations, or other pertinent transaction records. Personal information or unrelated transaction details should be redacted to maintain privacy.",
|
||||||
"Data Policy": "..."
|
"Non eligible countries": "USA citizens and residents are not allowed to use the Experimental coordinator. F2F transactions are explicitly blocked at creation time for US locations. If a US citizen or resident violates this rule and is found out to be using the Experimental coordinator during a dispute process, they will be denied further service and the dispute mediation will be terminated."
|
||||||
},
|
},
|
||||||
"mainnet": {
|
"mainnet": {
|
||||||
"onion": "http://robosats6tkf3eva7x2voqso3a5wcorsnw34jveyxfqi2fu7oyheasid.onion",
|
"onion": "http://robosats6tkf3eva7x2voqso3a5wcorsnw34jveyxfqi2fu7oyheasid.onion",
|
||||||
|
@ -464,8 +464,8 @@
|
|||||||
"The order has expired": "L'ordre ha expirat",
|
"The order has expired": "L'ordre ha expirat",
|
||||||
"The pinned location is approximate. The exact location for the meeting place must be exchanged in the encrypted chat.": "The pinned location is approximate. The exact location for the meeting place must be exchanged in the encrypted chat.",
|
"The pinned location is approximate. The exact location for the meeting place must be exchanged in the encrypted chat.": "The pinned location is approximate. The exact location for the meeting place must be exchanged in the encrypted chat.",
|
||||||
"You cannot take an order yet! Wait {{timeMin}}m {{timeSec}}s": "Encara no pots prendre cap ordre! Espera {{timeMin}}m {{timeSec}}s",
|
"You cannot take an order yet! Wait {{timeMin}}m {{timeSec}}s": "Encara no pots prendre cap ordre! Espera {{timeMin}}m {{timeSec}}s",
|
||||||
"You receive via Lightning {{amount}} Sats (Approx)": "Tu reps via Lightning {{amount}} Sats (Approx)",
|
|
||||||
"You receive via {{method}} {{amount}}": "Reps via {{method}} {{amount}}",
|
"You receive via {{method}} {{amount}}": "Reps via {{method}} {{amount}}",
|
||||||
|
"You receive {{amount}} Sats (Approx)": "Tu reps via Lightning {{amount}} Sats (Approx)",
|
||||||
"You send via Lightning {{amount}} Sats (Approx)": "Tu envies via Lightning {{amount}} Sats (Approx)",
|
"You send via Lightning {{amount}} Sats (Approx)": "Tu envies via Lightning {{amount}} Sats (Approx)",
|
||||||
"You send via {{method}} {{amount}}": "Envies via {{method}} {{amount}}",
|
"You send via {{method}} {{amount}}": "Envies via {{method}} {{amount}}",
|
||||||
"{{price}} {{currencyCode}}/BTC - Premium: {{premium}}%": "{{price}} {{currencyCode}}/BTC - Prima: {{premium}}%",
|
"{{price}} {{currencyCode}}/BTC - Premium: {{premium}}%": "{{price}} {{currencyCode}}/BTC - Prima: {{premium}}%",
|
||||||
@ -489,7 +489,9 @@
|
|||||||
"Your last order #{{orderID}}": "La teva última ordre #{{orderID}}",
|
"Your last order #{{orderID}}": "La teva última ordre #{{orderID}}",
|
||||||
"finished order": "finished order",
|
"finished order": "finished order",
|
||||||
"#43": "Phrases in components/SettingsForm/index.tsx",
|
"#43": "Phrases in components/SettingsForm/index.tsx",
|
||||||
|
"Build-in": "Build-in",
|
||||||
"Dark": "Fosc",
|
"Dark": "Fosc",
|
||||||
|
"Disabled": "Disabled",
|
||||||
"Fiat": "Fiat",
|
"Fiat": "Fiat",
|
||||||
"Light": "Clar",
|
"Light": "Clar",
|
||||||
"Mainnet": "Mainnet",
|
"Mainnet": "Mainnet",
|
||||||
|
@ -464,8 +464,8 @@
|
|||||||
"The order has expired": "Nabídka vypršela",
|
"The order has expired": "Nabídka vypršela",
|
||||||
"The pinned location is approximate. The exact location for the meeting place must be exchanged in the encrypted chat.": "The pinned location is approximate. The exact location for the meeting place must be exchanged in the encrypted chat.",
|
"The pinned location is approximate. The exact location for the meeting place must be exchanged in the encrypted chat.": "The pinned location is approximate. The exact location for the meeting place must be exchanged in the encrypted chat.",
|
||||||
"You cannot take an order yet! Wait {{timeMin}}m {{timeSec}}s": "Nabídku nemůžeš zatím příjmout! Počkej {{timeMin}}m {{timeSec}}s",
|
"You cannot take an order yet! Wait {{timeMin}}m {{timeSec}}s": "Nabídku nemůžeš zatím příjmout! Počkej {{timeMin}}m {{timeSec}}s",
|
||||||
"You receive via Lightning {{amount}} Sats (Approx)": "You receive via Lightning {{amount}} Sats (Approx)",
|
|
||||||
"You receive via {{method}} {{amount}}": "You receive via {{method}} {{amount}}",
|
"You receive via {{method}} {{amount}}": "You receive via {{method}} {{amount}}",
|
||||||
|
"You receive {{amount}} Sats (Approx)": "You receive {{amount}} Sats (Approx)",
|
||||||
"You send via Lightning {{amount}} Sats (Approx)": "You send via Lightning {{amount}} Sats (Approx)",
|
"You send via Lightning {{amount}} Sats (Approx)": "You send via Lightning {{amount}} Sats (Approx)",
|
||||||
"You send via {{method}} {{amount}}": "You send via {{method}} {{amount}}",
|
"You send via {{method}} {{amount}}": "You send via {{method}} {{amount}}",
|
||||||
"{{price}} {{currencyCode}}/BTC - Premium: {{premium}}%": "{{price}} {{currencyCode}}/BTC - Přirážka: {{premium}}%",
|
"{{price}} {{currencyCode}}/BTC - Premium: {{premium}}%": "{{price}} {{currencyCode}}/BTC - Přirážka: {{premium}}%",
|
||||||
@ -489,7 +489,9 @@
|
|||||||
"Your last order #{{orderID}}": "Tvá poslední nabídka #{{orderID}}",
|
"Your last order #{{orderID}}": "Tvá poslední nabídka #{{orderID}}",
|
||||||
"finished order": "finished order",
|
"finished order": "finished order",
|
||||||
"#43": "Phrases in components/SettingsForm/index.tsx",
|
"#43": "Phrases in components/SettingsForm/index.tsx",
|
||||||
|
"Build-in": "Build-in",
|
||||||
"Dark": "Dark",
|
"Dark": "Dark",
|
||||||
|
"Disabled": "Disabled",
|
||||||
"Fiat": "Fiat",
|
"Fiat": "Fiat",
|
||||||
"Light": "Light",
|
"Light": "Light",
|
||||||
"Mainnet": "Mainnet",
|
"Mainnet": "Mainnet",
|
||||||
|
@ -464,8 +464,8 @@
|
|||||||
"The order has expired": "Die Order ist abgelaufen",
|
"The order has expired": "Die Order ist abgelaufen",
|
||||||
"The pinned location is approximate. The exact location for the meeting place must be exchanged in the encrypted chat.": "The pinned location is approximate. The exact location for the meeting place must be exchanged in the encrypted chat.",
|
"The pinned location is approximate. The exact location for the meeting place must be exchanged in the encrypted chat.": "The pinned location is approximate. The exact location for the meeting place must be exchanged in the encrypted chat.",
|
||||||
"You cannot take an order yet! Wait {{timeMin}}m {{timeSec}}s": "Du kannst noch keine Order annehmen! Warte {{timeMin}}m {{timeSec}}s",
|
"You cannot take an order yet! Wait {{timeMin}}m {{timeSec}}s": "Du kannst noch keine Order annehmen! Warte {{timeMin}}m {{timeSec}}s",
|
||||||
"You receive via Lightning {{amount}} Sats (Approx)": "You receive via Lightning {{amount}} Sats (Approx)",
|
|
||||||
"You receive via {{method}} {{amount}}": "You receive via {{method}} {{amount}}",
|
"You receive via {{method}} {{amount}}": "You receive via {{method}} {{amount}}",
|
||||||
|
"You receive {{amount}} Sats (Approx)": "You receive {{amount}} Sats (Approx)",
|
||||||
"You send via Lightning {{amount}} Sats (Approx)": "You send via Lightning {{amount}} Sats (Approx)",
|
"You send via Lightning {{amount}} Sats (Approx)": "You send via Lightning {{amount}} Sats (Approx)",
|
||||||
"You send via {{method}} {{amount}}": "You send via {{method}} {{amount}}",
|
"You send via {{method}} {{amount}}": "You send via {{method}} {{amount}}",
|
||||||
"{{price}} {{currencyCode}}/BTC - Premium: {{premium}}%": "{{price}} {{currencyCode}}/BTC - Aufschlag: {{premium}}%",
|
"{{price}} {{currencyCode}}/BTC - Premium: {{premium}}%": "{{price}} {{currencyCode}}/BTC - Aufschlag: {{premium}}%",
|
||||||
@ -489,7 +489,9 @@
|
|||||||
"Your last order #{{orderID}}": "Deine letzte Order #{{orderID}}",
|
"Your last order #{{orderID}}": "Deine letzte Order #{{orderID}}",
|
||||||
"finished order": "finished order",
|
"finished order": "finished order",
|
||||||
"#43": "Phrases in components/SettingsForm/index.tsx",
|
"#43": "Phrases in components/SettingsForm/index.tsx",
|
||||||
|
"Build-in": "Build-in",
|
||||||
"Dark": "Dark",
|
"Dark": "Dark",
|
||||||
|
"Disabled": "Disabled",
|
||||||
"Fiat": "Fiat",
|
"Fiat": "Fiat",
|
||||||
"Light": "Light",
|
"Light": "Light",
|
||||||
"Mainnet": "Mainnet",
|
"Mainnet": "Mainnet",
|
||||||
|
@ -464,8 +464,8 @@
|
|||||||
"The order has expired": "The order has expired",
|
"The order has expired": "The order has expired",
|
||||||
"The pinned location is approximate. The exact location for the meeting place must be exchanged in the encrypted chat.": "The pinned location is approximate. The exact location for the meeting place must be exchanged in the encrypted chat.",
|
"The pinned location is approximate. The exact location for the meeting place must be exchanged in the encrypted chat.": "The pinned location is approximate. The exact location for the meeting place must be exchanged in the encrypted chat.",
|
||||||
"You cannot take an order yet! Wait {{timeMin}}m {{timeSec}}s": "You cannot take an order yet! Wait {{timeMin}}m {{timeSec}}s",
|
"You cannot take an order yet! Wait {{timeMin}}m {{timeSec}}s": "You cannot take an order yet! Wait {{timeMin}}m {{timeSec}}s",
|
||||||
"You receive via Lightning {{amount}} Sats (Approx)": "You receive via Lightning {{amount}} Sats (Approx)",
|
|
||||||
"You receive via {{method}} {{amount}}": "You receive via {{method}} {{amount}}",
|
"You receive via {{method}} {{amount}}": "You receive via {{method}} {{amount}}",
|
||||||
|
"You receive {{amount}} Sats (Approx)": "You receive {{amount}} Sats (Approx)",
|
||||||
"You send via Lightning {{amount}} Sats (Approx)": "You send via Lightning {{amount}} Sats (Approx)",
|
"You send via Lightning {{amount}} Sats (Approx)": "You send via Lightning {{amount}} Sats (Approx)",
|
||||||
"You send via {{method}} {{amount}}": "You send via {{method}} {{amount}}",
|
"You send via {{method}} {{amount}}": "You send via {{method}} {{amount}}",
|
||||||
"{{price}} {{currencyCode}}/BTC - Premium: {{premium}}%": "{{price}} {{currencyCode}}/BTC - Premium: {{premium}}%",
|
"{{price}} {{currencyCode}}/BTC - Premium: {{premium}}%": "{{price}} {{currencyCode}}/BTC - Premium: {{premium}}%",
|
||||||
@ -489,7 +489,9 @@
|
|||||||
"Your last order #{{orderID}}": "Your last order #{{orderID}}",
|
"Your last order #{{orderID}}": "Your last order #{{orderID}}",
|
||||||
"finished order": "finished order",
|
"finished order": "finished order",
|
||||||
"#43": "Phrases in components/SettingsForm/index.tsx",
|
"#43": "Phrases in components/SettingsForm/index.tsx",
|
||||||
|
"Build-in": "Build-in",
|
||||||
"Dark": "Dark",
|
"Dark": "Dark",
|
||||||
|
"Disabled": "Disabled",
|
||||||
"Fiat": "Fiat",
|
"Fiat": "Fiat",
|
||||||
"Light": "Light",
|
"Light": "Light",
|
||||||
"Mainnet": "Mainnet",
|
"Mainnet": "Mainnet",
|
||||||
|
@ -464,8 +464,8 @@
|
|||||||
"The order has expired": "La orden ha expirado",
|
"The order has expired": "La orden ha expirado",
|
||||||
"The pinned location is approximate. The exact location for the meeting place must be exchanged in the encrypted chat.": "The pinned location is approximate. The exact location for the meeting place must be exchanged in the encrypted chat.",
|
"The pinned location is approximate. The exact location for the meeting place must be exchanged in the encrypted chat.": "The pinned location is approximate. The exact location for the meeting place must be exchanged in the encrypted chat.",
|
||||||
"You cannot take an order yet! Wait {{timeMin}}m {{timeSec}}s": "¡No puedes tomar una orden aún! Espera {{timeMin}}m {{timeSec}}s",
|
"You cannot take an order yet! Wait {{timeMin}}m {{timeSec}}s": "¡No puedes tomar una orden aún! Espera {{timeMin}}m {{timeSec}}s",
|
||||||
"You receive via Lightning {{amount}} Sats (Approx)": "You receive via Lightning {{amount}} Sats (Approx)",
|
|
||||||
"You receive via {{method}} {{amount}}": "You receive via {{method}} {{amount}}",
|
"You receive via {{method}} {{amount}}": "You receive via {{method}} {{amount}}",
|
||||||
|
"You receive {{amount}} Sats (Approx)": "You receive {{amount}} Sats (Approx)",
|
||||||
"You send via Lightning {{amount}} Sats (Approx)": "You send via Lightning {{amount}} Sats (Approx)",
|
"You send via Lightning {{amount}} Sats (Approx)": "You send via Lightning {{amount}} Sats (Approx)",
|
||||||
"You send via {{method}} {{amount}}": "You send via {{method}} {{amount}}",
|
"You send via {{method}} {{amount}}": "You send via {{method}} {{amount}}",
|
||||||
"{{price}} {{currencyCode}}/BTC - Premium: {{premium}}%": "{{price}} {{currencyCode}}/BTC - Prima: {{premium}}%",
|
"{{price}} {{currencyCode}}/BTC - Premium: {{premium}}%": "{{price}} {{currencyCode}}/BTC - Prima: {{premium}}%",
|
||||||
@ -489,7 +489,9 @@
|
|||||||
"Your last order #{{orderID}}": "Tu última orden #{{orderID}}",
|
"Your last order #{{orderID}}": "Tu última orden #{{orderID}}",
|
||||||
"finished order": "finished order",
|
"finished order": "finished order",
|
||||||
"#43": "Phrases in components/SettingsForm/index.tsx",
|
"#43": "Phrases in components/SettingsForm/index.tsx",
|
||||||
|
"Build-in": "Build-in",
|
||||||
"Dark": "Oscuro",
|
"Dark": "Oscuro",
|
||||||
|
"Disabled": "Disabled",
|
||||||
"Fiat": "Fiat",
|
"Fiat": "Fiat",
|
||||||
"Light": "Claro",
|
"Light": "Claro",
|
||||||
"Mainnet": "Mainnet",
|
"Mainnet": "Mainnet",
|
||||||
|
@ -464,8 +464,8 @@
|
|||||||
"The order has expired": "Eskaera iraungi da",
|
"The order has expired": "Eskaera iraungi da",
|
||||||
"The pinned location is approximate. The exact location for the meeting place must be exchanged in the encrypted chat.": "The pinned location is approximate. The exact location for the meeting place must be exchanged in the encrypted chat.",
|
"The pinned location is approximate. The exact location for the meeting place must be exchanged in the encrypted chat.": "The pinned location is approximate. The exact location for the meeting place must be exchanged in the encrypted chat.",
|
||||||
"You cannot take an order yet! Wait {{timeMin}}m {{timeSec}}s": "Oraindik ezin duzu eskaerarik hartu! Itxaron{{timeMin}}m {{timeSec}}s",
|
"You cannot take an order yet! Wait {{timeMin}}m {{timeSec}}s": "Oraindik ezin duzu eskaerarik hartu! Itxaron{{timeMin}}m {{timeSec}}s",
|
||||||
"You receive via Lightning {{amount}} Sats (Approx)": "You receive via Lightning {{amount}} Sats (Approx)",
|
|
||||||
"You receive via {{method}} {{amount}}": "You receive via {{method}} {{amount}}",
|
"You receive via {{method}} {{amount}}": "You receive via {{method}} {{amount}}",
|
||||||
|
"You receive {{amount}} Sats (Approx)": "You receive {{amount}} Sats (Approx)",
|
||||||
"You send via Lightning {{amount}} Sats (Approx)": "You send via Lightning {{amount}} Sats (Approx)",
|
"You send via Lightning {{amount}} Sats (Approx)": "You send via Lightning {{amount}} Sats (Approx)",
|
||||||
"You send via {{method}} {{amount}}": "You send via {{method}} {{amount}}",
|
"You send via {{method}} {{amount}}": "You send via {{method}} {{amount}}",
|
||||||
"{{price}} {{currencyCode}}/BTC - Premium: {{premium}}%": "{{price}} {{currencyCode}}/BTC - Prima: %{{premium}}",
|
"{{price}} {{currencyCode}}/BTC - Premium: {{premium}}%": "{{price}} {{currencyCode}}/BTC - Prima: %{{premium}}",
|
||||||
@ -489,7 +489,9 @@
|
|||||||
"Your last order #{{orderID}}": "Zure azken eskaera #{{orderID}}",
|
"Your last order #{{orderID}}": "Zure azken eskaera #{{orderID}}",
|
||||||
"finished order": "finished order",
|
"finished order": "finished order",
|
||||||
"#43": "Phrases in components/SettingsForm/index.tsx",
|
"#43": "Phrases in components/SettingsForm/index.tsx",
|
||||||
|
"Build-in": "Build-in",
|
||||||
"Dark": "Dark",
|
"Dark": "Dark",
|
||||||
|
"Disabled": "Disabled",
|
||||||
"Fiat": "Fiat",
|
"Fiat": "Fiat",
|
||||||
"Light": "Light",
|
"Light": "Light",
|
||||||
"Mainnet": "Mainnet",
|
"Mainnet": "Mainnet",
|
||||||
|
@ -464,8 +464,8 @@
|
|||||||
"The order has expired": "L'ordre a expiré",
|
"The order has expired": "L'ordre a expiré",
|
||||||
"The pinned location is approximate. The exact location for the meeting place must be exchanged in the encrypted chat.": "The pinned location is approximate. The exact location for the meeting place must be exchanged in the encrypted chat.",
|
"The pinned location is approximate. The exact location for the meeting place must be exchanged in the encrypted chat.": "The pinned location is approximate. The exact location for the meeting place must be exchanged in the encrypted chat.",
|
||||||
"You cannot take an order yet! Wait {{timeMin}}m {{timeSec}}s": "Vous ne pouvez pas encore prendre un ordre! Attendez {{timeMin}}m {{timeSec}}s",
|
"You cannot take an order yet! Wait {{timeMin}}m {{timeSec}}s": "Vous ne pouvez pas encore prendre un ordre! Attendez {{timeMin}}m {{timeSec}}s",
|
||||||
"You receive via Lightning {{amount}} Sats (Approx)": "Vous recevez via Lightning {{amount}} Sats (environ)",
|
|
||||||
"You receive via {{method}} {{amount}}": "Vous recevez via {{méthode}} {{montant}}",
|
"You receive via {{method}} {{amount}}": "Vous recevez via {{méthode}} {{montant}}",
|
||||||
|
"You receive {{amount}} Sats (Approx)": "Vous recevez via Lightning {{amount}} Sats (environ)",
|
||||||
"You send via Lightning {{amount}} Sats (Approx)": "Vous envoyez via Lightning {{amount}} Sats (environ)",
|
"You send via Lightning {{amount}} Sats (Approx)": "Vous envoyez via Lightning {{amount}} Sats (environ)",
|
||||||
"You send via {{method}} {{amount}}": "Vous envoyez via {{method}} {{amount}}",
|
"You send via {{method}} {{amount}}": "Vous envoyez via {{method}} {{amount}}",
|
||||||
"{{price}} {{currencyCode}}/BTC - Premium: {{premium}}%": "{{price}} {{currencyCode}}/BTC - Prime: {{premium}}%",
|
"{{price}} {{currencyCode}}/BTC - Premium: {{premium}}%": "{{price}} {{currencyCode}}/BTC - Prime: {{premium}}%",
|
||||||
@ -489,7 +489,9 @@
|
|||||||
"Your last order #{{orderID}}": "Votre dernière commande #{{orderID}}",
|
"Your last order #{{orderID}}": "Votre dernière commande #{{orderID}}",
|
||||||
"finished order": "finished order",
|
"finished order": "finished order",
|
||||||
"#43": "Phrases in components/SettingsForm/index.tsx",
|
"#43": "Phrases in components/SettingsForm/index.tsx",
|
||||||
|
"Build-in": "Build-in",
|
||||||
"Dark": "Sombre",
|
"Dark": "Sombre",
|
||||||
|
"Disabled": "Disabled",
|
||||||
"Fiat": "Fiat",
|
"Fiat": "Fiat",
|
||||||
"Light": "Light",
|
"Light": "Light",
|
||||||
"Mainnet": "Mainnet",
|
"Mainnet": "Mainnet",
|
||||||
|
@ -464,8 +464,8 @@
|
|||||||
"The order has expired": "L'ordine è scaduto",
|
"The order has expired": "L'ordine è scaduto",
|
||||||
"The pinned location is approximate. The exact location for the meeting place must be exchanged in the encrypted chat.": "La posizione appuntata è approssimativa. La posizione esatta del luogo dell'incontro deve essere indicata nella chat crittografata.",
|
"The pinned location is approximate. The exact location for the meeting place must be exchanged in the encrypted chat.": "La posizione appuntata è approssimativa. La posizione esatta del luogo dell'incontro deve essere indicata nella chat crittografata.",
|
||||||
"You cannot take an order yet! Wait {{timeMin}}m {{timeSec}}s": "Non puoi ancora accettare un ordine! Aspetta {{timeMin}}m {{timeSec}}s",
|
"You cannot take an order yet! Wait {{timeMin}}m {{timeSec}}s": "Non puoi ancora accettare un ordine! Aspetta {{timeMin}}m {{timeSec}}s",
|
||||||
"You receive via Lightning {{amount}} Sats (Approx)": "Ricevi {{amount}} Sats via Lightning (approssimativo)",
|
|
||||||
"You receive via {{method}} {{amount}}": "Ricevi {{amount}} via {{method}}",
|
"You receive via {{method}} {{amount}}": "Ricevi {{amount}} via {{method}}",
|
||||||
|
"You receive {{amount}} Sats (Approx)": "Ricevi {{amount}} Sats via Lightning (approssimativo)",
|
||||||
"You send via Lightning {{amount}} Sats (Approx)": "Invii {{amount}} Sats via Lightning (approssimativo)",
|
"You send via Lightning {{amount}} Sats (Approx)": "Invii {{amount}} Sats via Lightning (approssimativo)",
|
||||||
"You send via {{method}} {{amount}}": "Invii {{amount}} via {{method}}",
|
"You send via {{method}} {{amount}}": "Invii {{amount}} via {{method}}",
|
||||||
"{{price}} {{currencyCode}}/BTC - Premium: {{premium}}%": "{{price}} {{currencyCode}}/BTC - Premio: {{premium}}%",
|
"{{price}} {{currencyCode}}/BTC - Premium: {{premium}}%": "{{price}} {{currencyCode}}/BTC - Premio: {{premium}}%",
|
||||||
@ -489,7 +489,9 @@
|
|||||||
"Your last order #{{orderID}}": "Il tuo ultimo ordine #{{orderID}}",
|
"Your last order #{{orderID}}": "Il tuo ultimo ordine #{{orderID}}",
|
||||||
"finished order": "finished order",
|
"finished order": "finished order",
|
||||||
"#43": "Phrases in components/SettingsForm/index.tsx",
|
"#43": "Phrases in components/SettingsForm/index.tsx",
|
||||||
|
"Build-in": "Build-in",
|
||||||
"Dark": "Scuro",
|
"Dark": "Scuro",
|
||||||
|
"Disabled": "Disabled",
|
||||||
"Fiat": "Fiat",
|
"Fiat": "Fiat",
|
||||||
"Light": "Chiaro",
|
"Light": "Chiaro",
|
||||||
"Mainnet": "Mainnet",
|
"Mainnet": "Mainnet",
|
||||||
|
@ -464,8 +464,8 @@
|
|||||||
"The order has expired": "注文は期限切れになりました",
|
"The order has expired": "注文は期限切れになりました",
|
||||||
"The pinned location is approximate. The exact location for the meeting place must be exchanged in the encrypted chat.": "The pinned location is approximate. The exact location for the meeting place must be exchanged in the encrypted chat.",
|
"The pinned location is approximate. The exact location for the meeting place must be exchanged in the encrypted chat.": "The pinned location is approximate. The exact location for the meeting place must be exchanged in the encrypted chat.",
|
||||||
"You cannot take an order yet! Wait {{timeMin}}m {{timeSec}}s": "まだ注文を受け取ることはできません!{{timeMin}}分{{timeSec}}秒待ってください",
|
"You cannot take an order yet! Wait {{timeMin}}m {{timeSec}}s": "まだ注文を受け取ることはできません!{{timeMin}}分{{timeSec}}秒待ってください",
|
||||||
"You receive via Lightning {{amount}} Sats (Approx)": "ライトニングで{{amount}} Sats(約)を受け取ります",
|
|
||||||
"You receive via {{method}} {{amount}}": "{{method}}で{{amount}}を受け取ります",
|
"You receive via {{method}} {{amount}}": "{{method}}で{{amount}}を受け取ります",
|
||||||
|
"You receive {{amount}} Sats (Approx)": "ライトニングで{{amount}} Sats(約)を受け取ります",
|
||||||
"You send via Lightning {{amount}} Sats (Approx)": "ライトニングで{{amount}} Sats(約)を送信します",
|
"You send via Lightning {{amount}} Sats (Approx)": "ライトニングで{{amount}} Sats(約)を送信します",
|
||||||
"You send via {{method}} {{amount}}": "{{method}}で{{amount}}を送信します",
|
"You send via {{method}} {{amount}}": "{{method}}で{{amount}}を送信します",
|
||||||
"{{price}} {{currencyCode}}/BTC - Premium: {{premium}}%": "{{price}} {{currencyCode}}/BTC - プレミアム: {{premium}}%",
|
"{{price}} {{currencyCode}}/BTC - Premium: {{premium}}%": "{{price}} {{currencyCode}}/BTC - プレミアム: {{premium}}%",
|
||||||
@ -489,7 +489,9 @@
|
|||||||
"Your last order #{{orderID}}": "前回のオーダー #{{orderID}}",
|
"Your last order #{{orderID}}": "前回のオーダー #{{orderID}}",
|
||||||
"finished order": "finished order",
|
"finished order": "finished order",
|
||||||
"#43": "Phrases in components/SettingsForm/index.tsx",
|
"#43": "Phrases in components/SettingsForm/index.tsx",
|
||||||
|
"Build-in": "Build-in",
|
||||||
"Dark": "ダーク",
|
"Dark": "ダーク",
|
||||||
|
"Disabled": "Disabled",
|
||||||
"Fiat": "フィアット",
|
"Fiat": "フィアット",
|
||||||
"Light": "ライト",
|
"Light": "ライト",
|
||||||
"Mainnet": "メインネット",
|
"Mainnet": "メインネット",
|
||||||
|
@ -464,8 +464,8 @@
|
|||||||
"The order has expired": "Zamówienie wygasło",
|
"The order has expired": "Zamówienie wygasło",
|
||||||
"The pinned location is approximate. The exact location for the meeting place must be exchanged in the encrypted chat.": "The pinned location is approximate. The exact location for the meeting place must be exchanged in the encrypted chat.",
|
"The pinned location is approximate. The exact location for the meeting place must be exchanged in the encrypted chat.": "The pinned location is approximate. The exact location for the meeting place must be exchanged in the encrypted chat.",
|
||||||
"You cannot take an order yet! Wait {{timeMin}}m {{timeSec}}s": "Nie możesz jeszcze przyjąć zamówienia! Czekać {{timeMin}}m {{timeSec}}s",
|
"You cannot take an order yet! Wait {{timeMin}}m {{timeSec}}s": "Nie możesz jeszcze przyjąć zamówienia! Czekać {{timeMin}}m {{timeSec}}s",
|
||||||
"You receive via Lightning {{amount}} Sats (Approx)": "You receive via Lightning {{amount}} Sats (Approx)",
|
|
||||||
"You receive via {{method}} {{amount}}": "You receive via {{method}} {{amount}}",
|
"You receive via {{method}} {{amount}}": "You receive via {{method}} {{amount}}",
|
||||||
|
"You receive {{amount}} Sats (Approx)": "You receive {{amount}} Sats (Approx)",
|
||||||
"You send via Lightning {{amount}} Sats (Approx)": "You send via Lightning {{amount}} Sats (Approx)",
|
"You send via Lightning {{amount}} Sats (Approx)": "You send via Lightning {{amount}} Sats (Approx)",
|
||||||
"You send via {{method}} {{amount}}": "You send via {{method}} {{amount}}",
|
"You send via {{method}} {{amount}}": "You send via {{method}} {{amount}}",
|
||||||
"{{price}} {{currencyCode}}/BTC - Premium: {{premium}}%": "{{price}} {{currencyCode}}/BTC - Premia: {{premium}}%",
|
"{{price}} {{currencyCode}}/BTC - Premium: {{premium}}%": "{{price}} {{currencyCode}}/BTC - Premia: {{premium}}%",
|
||||||
@ -489,7 +489,9 @@
|
|||||||
"Your last order #{{orderID}}": "Your last order #{{orderID}}",
|
"Your last order #{{orderID}}": "Your last order #{{orderID}}",
|
||||||
"finished order": "finished order",
|
"finished order": "finished order",
|
||||||
"#43": "Phrases in components/SettingsForm/index.tsx",
|
"#43": "Phrases in components/SettingsForm/index.tsx",
|
||||||
|
"Build-in": "Build-in",
|
||||||
"Dark": "Dark",
|
"Dark": "Dark",
|
||||||
|
"Disabled": "Disabled",
|
||||||
"Fiat": "Fiat",
|
"Fiat": "Fiat",
|
||||||
"Light": "Light",
|
"Light": "Light",
|
||||||
"Mainnet": "Mainnet",
|
"Mainnet": "Mainnet",
|
||||||
|
@ -464,8 +464,8 @@
|
|||||||
"The order has expired": "A ordem expirou",
|
"The order has expired": "A ordem expirou",
|
||||||
"The pinned location is approximate. The exact location for the meeting place must be exchanged in the encrypted chat.": "The pinned location is approximate. The exact location for the meeting place must be exchanged in the encrypted chat.",
|
"The pinned location is approximate. The exact location for the meeting place must be exchanged in the encrypted chat.": "The pinned location is approximate. The exact location for the meeting place must be exchanged in the encrypted chat.",
|
||||||
"You cannot take an order yet! Wait {{timeMin}}m {{timeSec}}s": "Você ainda não pode fazer um pedido! Espere {{timeMin}}m {{timeSec}}s",
|
"You cannot take an order yet! Wait {{timeMin}}m {{timeSec}}s": "Você ainda não pode fazer um pedido! Espere {{timeMin}}m {{timeSec}}s",
|
||||||
"You receive via Lightning {{amount}} Sats (Approx)": "You receive via Lightning {{amount}} Sats (Approx)",
|
|
||||||
"You receive via {{method}} {{amount}}": "You receive via {{method}} {{amount}}",
|
"You receive via {{method}} {{amount}}": "You receive via {{method}} {{amount}}",
|
||||||
|
"You receive {{amount}} Sats (Approx)": "You receive {{amount}} Sats (Approx)",
|
||||||
"You send via Lightning {{amount}} Sats (Approx)": "You send via Lightning {{amount}} Sats (Approx)",
|
"You send via Lightning {{amount}} Sats (Approx)": "You send via Lightning {{amount}} Sats (Approx)",
|
||||||
"You send via {{method}} {{amount}}": "You send via {{method}} {{amount}}",
|
"You send via {{method}} {{amount}}": "You send via {{method}} {{amount}}",
|
||||||
"{{price}} {{currencyCode}}/BTC - Premium: {{premium}}%": "{{price}} {{currencyCode}}/BTC - Prêmio: {{premium}}%",
|
"{{price}} {{currencyCode}}/BTC - Premium: {{premium}}%": "{{price}} {{currencyCode}}/BTC - Prêmio: {{premium}}%",
|
||||||
@ -489,7 +489,9 @@
|
|||||||
"Your last order #{{orderID}}": "Sua última ordem #{{orderID}}",
|
"Your last order #{{orderID}}": "Sua última ordem #{{orderID}}",
|
||||||
"finished order": "ordem finalizada",
|
"finished order": "ordem finalizada",
|
||||||
"#43": "Phrases in components/SettingsForm/index.tsx",
|
"#43": "Phrases in components/SettingsForm/index.tsx",
|
||||||
|
"Build-in": "Build-in",
|
||||||
"Dark": "Dark",
|
"Dark": "Dark",
|
||||||
|
"Disabled": "Disabled",
|
||||||
"Fiat": "Fiat",
|
"Fiat": "Fiat",
|
||||||
"Light": "Light",
|
"Light": "Light",
|
||||||
"Mainnet": "Mainnet",
|
"Mainnet": "Mainnet",
|
||||||
|
@ -464,8 +464,8 @@
|
|||||||
"The order has expired": "Срок действия ордера истёк",
|
"The order has expired": "Срок действия ордера истёк",
|
||||||
"The pinned location is approximate. The exact location for the meeting place must be exchanged in the encrypted chat.": "Закрепленное местоположение является приблизительным. Точное местоположение места встречи необходимо сообщить в зашифрованном чате.",
|
"The pinned location is approximate. The exact location for the meeting place must be exchanged in the encrypted chat.": "Закрепленное местоположение является приблизительным. Точное местоположение места встречи необходимо сообщить в зашифрованном чате.",
|
||||||
"You cannot take an order yet! Wait {{timeMin}}m {{timeSec}}s": "Вы ещё не можете взять ордер! Подождите {{timeMin}}м {{timeSec}}с",
|
"You cannot take an order yet! Wait {{timeMin}}m {{timeSec}}s": "Вы ещё не можете взять ордер! Подождите {{timeMin}}м {{timeSec}}с",
|
||||||
"You receive via Lightning {{amount}} Sats (Approx)": "Вы получаете через Lightning {{amount}} Сатоши (приблизительно)",
|
|
||||||
"You receive via {{method}} {{amount}}": "Вы получаете через {{method}} {{amount}}",
|
"You receive via {{method}} {{amount}}": "Вы получаете через {{method}} {{amount}}",
|
||||||
|
"You receive {{amount}} Sats (Approx)": "Вы получаете через Lightning {{amount}} Сатоши (приблизительно)",
|
||||||
"You send via Lightning {{amount}} Sats (Approx)": "Вы отправляете через Lightning {{amount}} Сатоши (приблизительно)",
|
"You send via Lightning {{amount}} Sats (Approx)": "Вы отправляете через Lightning {{amount}} Сатоши (приблизительно)",
|
||||||
"You send via {{method}} {{amount}}": "Вы отправляете через {{method}} {{amount}}",
|
"You send via {{method}} {{amount}}": "Вы отправляете через {{method}} {{amount}}",
|
||||||
"{{price}} {{currencyCode}}/BTC - Premium: {{premium}}%": "{{price}} {{currencyCode}}/BTC - Наценка: {{premium}}%",
|
"{{price}} {{currencyCode}}/BTC - Premium: {{premium}}%": "{{price}} {{currencyCode}}/BTC - Наценка: {{premium}}%",
|
||||||
@ -489,7 +489,9 @@
|
|||||||
"Your last order #{{orderID}}": "Ваш последний ордер #{{orderID}}",
|
"Your last order #{{orderID}}": "Ваш последний ордер #{{orderID}}",
|
||||||
"finished order": "finished order",
|
"finished order": "finished order",
|
||||||
"#43": "Phrases in components/SettingsForm/index.tsx",
|
"#43": "Phrases in components/SettingsForm/index.tsx",
|
||||||
|
"Build-in": "Build-in",
|
||||||
"Dark": "Темный",
|
"Dark": "Темный",
|
||||||
|
"Disabled": "Disabled",
|
||||||
"Fiat": "Фиат",
|
"Fiat": "Фиат",
|
||||||
"Light": "Светлый",
|
"Light": "Светлый",
|
||||||
"Mainnet": "Основная сеть",
|
"Mainnet": "Основная сеть",
|
||||||
|
@ -464,8 +464,8 @@
|
|||||||
"The order has expired": "Ordern har förfallit",
|
"The order has expired": "Ordern har förfallit",
|
||||||
"The pinned location is approximate. The exact location for the meeting place must be exchanged in the encrypted chat.": "The pinned location is approximate. The exact location for the meeting place must be exchanged in the encrypted chat.",
|
"The pinned location is approximate. The exact location for the meeting place must be exchanged in the encrypted chat.": "The pinned location is approximate. The exact location for the meeting place must be exchanged in the encrypted chat.",
|
||||||
"You cannot take an order yet! Wait {{timeMin}}m {{timeSec}}s": "Du kan inte ta en order ännu! Vänta {{timeMin}}m {{timeSec}}s",
|
"You cannot take an order yet! Wait {{timeMin}}m {{timeSec}}s": "Du kan inte ta en order ännu! Vänta {{timeMin}}m {{timeSec}}s",
|
||||||
"You receive via Lightning {{amount}} Sats (Approx)": "You receive via Lightning {{amount}} Sats (Approx)",
|
|
||||||
"You receive via {{method}} {{amount}}": "You receive via {{method}} {{amount}}",
|
"You receive via {{method}} {{amount}}": "You receive via {{method}} {{amount}}",
|
||||||
|
"You receive {{amount}} Sats (Approx)": "You receive {{amount}} Sats (Approx)",
|
||||||
"You send via Lightning {{amount}} Sats (Approx)": "You send via Lightning {{amount}} Sats (Approx)",
|
"You send via Lightning {{amount}} Sats (Approx)": "You send via Lightning {{amount}} Sats (Approx)",
|
||||||
"You send via {{method}} {{amount}}": "You send via {{method}} {{amount}}",
|
"You send via {{method}} {{amount}}": "You send via {{method}} {{amount}}",
|
||||||
"{{price}} {{currencyCode}}/BTC - Premium: {{premium}}%": "{{price}} {{currencyCode}}/BTC - Premium: {{premium}}%",
|
"{{price}} {{currencyCode}}/BTC - Premium: {{premium}}%": "{{price}} {{currencyCode}}/BTC - Premium: {{premium}}%",
|
||||||
@ -489,7 +489,9 @@
|
|||||||
"Your last order #{{orderID}}": "Din senaste order #{{orderID}}",
|
"Your last order #{{orderID}}": "Din senaste order #{{orderID}}",
|
||||||
"finished order": "finished order",
|
"finished order": "finished order",
|
||||||
"#43": "Phrases in components/SettingsForm/index.tsx",
|
"#43": "Phrases in components/SettingsForm/index.tsx",
|
||||||
|
"Build-in": "Build-in",
|
||||||
"Dark": "Dark",
|
"Dark": "Dark",
|
||||||
|
"Disabled": "Disabled",
|
||||||
"Fiat": "Fiat",
|
"Fiat": "Fiat",
|
||||||
"Light": "Light",
|
"Light": "Light",
|
||||||
"Mainnet": "Mainnet",
|
"Mainnet": "Mainnet",
|
||||||
|
@ -464,8 +464,8 @@
|
|||||||
"The order has expired": "Agizo limekwisha muda",
|
"The order has expired": "Agizo limekwisha muda",
|
||||||
"The pinned location is approximate. The exact location for the meeting place must be exchanged in the encrypted chat.": "The pinned location is approximate. The exact location for the meeting place must be exchanged in the encrypted chat.",
|
"The pinned location is approximate. The exact location for the meeting place must be exchanged in the encrypted chat.": "The pinned location is approximate. The exact location for the meeting place must be exchanged in the encrypted chat.",
|
||||||
"You cannot take an order yet! Wait {{timeMin}}m {{timeSec}}s": "Hauwezi kuchukua agizo bado! Subiri {{timeMin}}m {{timeSec}}s",
|
"You cannot take an order yet! Wait {{timeMin}}m {{timeSec}}s": "Hauwezi kuchukua agizo bado! Subiri {{timeMin}}m {{timeSec}}s",
|
||||||
"You receive via Lightning {{amount}} Sats (Approx)": "Utapokea kupitia Lightning {{amount}} Sats (Takriban)",
|
|
||||||
"You receive via {{method}} {{amount}}": "Utapokea kupitia {{method}} {{amount}}",
|
"You receive via {{method}} {{amount}}": "Utapokea kupitia {{method}} {{amount}}",
|
||||||
|
"You receive {{amount}} Sats (Approx)": "Utapokea kupitia Lightning {{amount}} Sats (Takriban)",
|
||||||
"You send via Lightning {{amount}} Sats (Approx)": "Utatuma kupitia Lightning {{amount}} Sats (Takriban)",
|
"You send via Lightning {{amount}} Sats (Approx)": "Utatuma kupitia Lightning {{amount}} Sats (Takriban)",
|
||||||
"You send via {{method}} {{amount}}": "Utatuma kupitia {{method}} {{amount}}",
|
"You send via {{method}} {{amount}}": "Utatuma kupitia {{method}} {{amount}}",
|
||||||
"{{price}} {{currencyCode}}/BTC - Premium: {{premium}}%": "{{price}} {{currencyCode}}/BTC - Premium: {{premium}}%",
|
"{{price}} {{currencyCode}}/BTC - Premium: {{premium}}%": "{{price}} {{currencyCode}}/BTC - Premium: {{premium}}%",
|
||||||
@ -489,7 +489,9 @@
|
|||||||
"Your last order #{{orderID}}": "Amri yako ya mwisho #{{orderID}}",
|
"Your last order #{{orderID}}": "Amri yako ya mwisho #{{orderID}}",
|
||||||
"finished order": "finished order",
|
"finished order": "finished order",
|
||||||
"#43": "Phrases in components/SettingsForm/index.tsx",
|
"#43": "Phrases in components/SettingsForm/index.tsx",
|
||||||
|
"Build-in": "Build-in",
|
||||||
"Dark": "Giza",
|
"Dark": "Giza",
|
||||||
|
"Disabled": "Disabled",
|
||||||
"Fiat": "Fiat",
|
"Fiat": "Fiat",
|
||||||
"Light": "Nuru",
|
"Light": "Nuru",
|
||||||
"Mainnet": "Mainnet",
|
"Mainnet": "Mainnet",
|
||||||
|
@ -464,8 +464,8 @@
|
|||||||
"The order has expired": "รายการหมดอายุแล้ว",
|
"The order has expired": "รายการหมดอายุแล้ว",
|
||||||
"The pinned location is approximate. The exact location for the meeting place must be exchanged in the encrypted chat.": "The pinned location is approximate. The exact location for the meeting place must be exchanged in the encrypted chat.",
|
"The pinned location is approximate. The exact location for the meeting place must be exchanged in the encrypted chat.": "The pinned location is approximate. The exact location for the meeting place must be exchanged in the encrypted chat.",
|
||||||
"You cannot take an order yet! Wait {{timeMin}}m {{timeSec}}s": "คุณยังไม่สามารถดำเนินรายการได้! รออีก {{timeMin}} นาที {{timeSec}} วินาที",
|
"You cannot take an order yet! Wait {{timeMin}}m {{timeSec}}s": "คุณยังไม่สามารถดำเนินรายการได้! รออีก {{timeMin}} นาที {{timeSec}} วินาที",
|
||||||
"You receive via Lightning {{amount}} Sats (Approx)": "You receive via Lightning {{amount}} Sats (Approx)",
|
|
||||||
"You receive via {{method}} {{amount}}": "You receive via {{method}} {{amount}}",
|
"You receive via {{method}} {{amount}}": "You receive via {{method}} {{amount}}",
|
||||||
|
"You receive {{amount}} Sats (Approx)": "You receive {{amount}} Sats (Approx)",
|
||||||
"You send via Lightning {{amount}} Sats (Approx)": "You send via Lightning {{amount}} Sats (Approx)",
|
"You send via Lightning {{amount}} Sats (Approx)": "You send via Lightning {{amount}} Sats (Approx)",
|
||||||
"You send via {{method}} {{amount}}": "You send via {{method}} {{amount}}",
|
"You send via {{method}} {{amount}}": "You send via {{method}} {{amount}}",
|
||||||
"{{price}} {{currencyCode}}/BTC - Premium: {{premium}}%": "{{price}} {{currencyCode}}/BTC - ค่าพรีเมี่ยม: {{premium}}%",
|
"{{price}} {{currencyCode}}/BTC - Premium: {{premium}}%": "{{price}} {{currencyCode}}/BTC - ค่าพรีเมี่ยม: {{premium}}%",
|
||||||
@ -489,7 +489,9 @@
|
|||||||
"Your last order #{{orderID}}": "รายการล่าสุดของคุณ #{{orderID}}",
|
"Your last order #{{orderID}}": "รายการล่าสุดของคุณ #{{orderID}}",
|
||||||
"finished order": "finished order",
|
"finished order": "finished order",
|
||||||
"#43": "Phrases in components/SettingsForm/index.tsx",
|
"#43": "Phrases in components/SettingsForm/index.tsx",
|
||||||
|
"Build-in": "Build-in",
|
||||||
"Dark": "Dark",
|
"Dark": "Dark",
|
||||||
|
"Disabled": "Disabled",
|
||||||
"Fiat": "Fiat",
|
"Fiat": "Fiat",
|
||||||
"Light": "Light",
|
"Light": "Light",
|
||||||
"Mainnet": "Mainnet",
|
"Mainnet": "Mainnet",
|
||||||
|
@ -464,8 +464,8 @@
|
|||||||
"The order has expired": "订单已到期",
|
"The order has expired": "订单已到期",
|
||||||
"The pinned location is approximate. The exact location for the meeting place must be exchanged in the encrypted chat.": "The pinned location is approximate. The exact location for the meeting place must be exchanged in the encrypted chat.",
|
"The pinned location is approximate. The exact location for the meeting place must be exchanged in the encrypted chat.": "The pinned location is approximate. The exact location for the meeting place must be exchanged in the encrypted chat.",
|
||||||
"You cannot take an order yet! Wait {{timeMin}}m {{timeSec}}s": "你暂时还不能吃单!请等{{timeMin}}分 {{timeSec}}秒",
|
"You cannot take an order yet! Wait {{timeMin}}m {{timeSec}}s": "你暂时还不能吃单!请等{{timeMin}}分 {{timeSec}}秒",
|
||||||
"You receive via Lightning {{amount}} Sats (Approx)": "You receive via Lightning {{amount}} Sats (Approx)",
|
|
||||||
"You receive via {{method}} {{amount}}": "你通过{{method}}接收{{amount}}",
|
"You receive via {{method}} {{amount}}": "你通过{{method}}接收{{amount}}",
|
||||||
|
"You receive {{amount}} Sats (Approx)": "You receive {{amount}} Sats (Approx)",
|
||||||
"You send via Lightning {{amount}} Sats (Approx)": "You send via Lightning {{amount}} Sats (Approx)",
|
"You send via Lightning {{amount}} Sats (Approx)": "You send via Lightning {{amount}} Sats (Approx)",
|
||||||
"You send via {{method}} {{amount}}": "你通过{{method}}发送{{amount}}",
|
"You send via {{method}} {{amount}}": "你通过{{method}}发送{{amount}}",
|
||||||
"{{price}} {{currencyCode}}/BTC - Premium: {{premium}}%": "{{price}} {{currencyCode}}/BTC - 溢价: {{premium}}%",
|
"{{price}} {{currencyCode}}/BTC - Premium: {{premium}}%": "{{price}} {{currencyCode}}/BTC - 溢价: {{premium}}%",
|
||||||
@ -489,7 +489,9 @@
|
|||||||
"Your last order #{{orderID}}": "你的上一笔交易 #{{orderID}}",
|
"Your last order #{{orderID}}": "你的上一笔交易 #{{orderID}}",
|
||||||
"finished order": "finished order",
|
"finished order": "finished order",
|
||||||
"#43": "Phrases in components/SettingsForm/index.tsx",
|
"#43": "Phrases in components/SettingsForm/index.tsx",
|
||||||
|
"Build-in": "Build-in",
|
||||||
"Dark": "深色",
|
"Dark": "深色",
|
||||||
|
"Disabled": "Disabled",
|
||||||
"Fiat": "法币",
|
"Fiat": "法币",
|
||||||
"Light": "浅色",
|
"Light": "浅色",
|
||||||
"Mainnet": "主网",
|
"Mainnet": "主网",
|
||||||
|
@ -464,8 +464,8 @@
|
|||||||
"The order has expired": "訂單已到期",
|
"The order has expired": "訂單已到期",
|
||||||
"The pinned location is approximate. The exact location for the meeting place must be exchanged in the encrypted chat.": "The pinned location is approximate. The exact location for the meeting place must be exchanged in the encrypted chat.",
|
"The pinned location is approximate. The exact location for the meeting place must be exchanged in the encrypted chat.": "The pinned location is approximate. The exact location for the meeting place must be exchanged in the encrypted chat.",
|
||||||
"You cannot take an order yet! Wait {{timeMin}}m {{timeSec}}s": "你暫時還不能吃單!請等{{timeMin}}分 {{timeSec}}秒",
|
"You cannot take an order yet! Wait {{timeMin}}m {{timeSec}}s": "你暫時還不能吃單!請等{{timeMin}}分 {{timeSec}}秒",
|
||||||
"You receive via Lightning {{amount}} Sats (Approx)": "You receive via Lightning {{amount}} Sats (Approx)",
|
|
||||||
"You receive via {{method}} {{amount}}": "你通過{{method}}接收{{amount}}",
|
"You receive via {{method}} {{amount}}": "你通過{{method}}接收{{amount}}",
|
||||||
|
"You receive {{amount}} Sats (Approx)": "You receive {{amount}} Sats (Approx)",
|
||||||
"You send via Lightning {{amount}} Sats (Approx)": "You send via Lightning {{amount}} Sats (Approx)",
|
"You send via Lightning {{amount}} Sats (Approx)": "You send via Lightning {{amount}} Sats (Approx)",
|
||||||
"You send via {{method}} {{amount}}": "你通過{{method}}發送{{amount}}",
|
"You send via {{method}} {{amount}}": "你通過{{method}}發送{{amount}}",
|
||||||
"{{price}} {{currencyCode}}/BTC - Premium: {{premium}}%": "{{price}} {{currencyCode}}/BTC - 溢價: {{premium}}%",
|
"{{price}} {{currencyCode}}/BTC - Premium: {{premium}}%": "{{price}} {{currencyCode}}/BTC - 溢價: {{premium}}%",
|
||||||
@ -489,7 +489,9 @@
|
|||||||
"Your last order #{{orderID}}": "你的上一筆交易 #{{orderID}}",
|
"Your last order #{{orderID}}": "你的上一筆交易 #{{orderID}}",
|
||||||
"finished order": "finished order",
|
"finished order": "finished order",
|
||||||
"#43": "Phrases in components/SettingsForm/index.tsx",
|
"#43": "Phrases in components/SettingsForm/index.tsx",
|
||||||
|
"Build-in": "Build-in",
|
||||||
"Dark": "深色",
|
"Dark": "深色",
|
||||||
|
"Disabled": "Disabled",
|
||||||
"Fiat": "法幣",
|
"Fiat": "法幣",
|
||||||
"Light": "淺色",
|
"Light": "淺色",
|
||||||
"Mainnet": "主網",
|
"Mainnet": "主網",
|
||||||
|
@ -56,6 +56,15 @@ const configMobile: Configuration = {
|
|||||||
async: true,
|
async: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
test: path.resolve(__dirname, 'src/services/Roboidentities/Web.ts'),
|
||||||
|
loader: 'file-replace-loader',
|
||||||
|
options: {
|
||||||
|
condition: 'if-replacement-exists',
|
||||||
|
replacement: path.resolve(__dirname, 'src/services/Roboidentities/Native.ts'),
|
||||||
|
async: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
test: path.resolve(__dirname, 'src/components/RobotAvatar/placeholder.json'),
|
test: path.resolve(__dirname, 'src/components/RobotAvatar/placeholder.json'),
|
||||||
loader: 'file-replace-loader',
|
loader: 'file-replace-loader',
|
||||||
@ -81,6 +90,10 @@ const configMobile: Configuration = {
|
|||||||
from: path.resolve(__dirname, 'static/assets/sounds'),
|
from: path.resolve(__dirname, 'static/assets/sounds'),
|
||||||
to: path.resolve(__dirname, '../mobile/html/Web.bundle/assets/sounds'),
|
to: path.resolve(__dirname, '../mobile/html/Web.bundle/assets/sounds'),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
from: path.resolve(__dirname, 'static/federation'),
|
||||||
|
to: path.resolve(__dirname, '../mobile/html/Web.bundle/assets/federation'),
|
||||||
|
},
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
|
@ -1,23 +1,45 @@
|
|||||||
import React, { useRef } from 'react';
|
import React, { useEffect, useRef } from 'react';
|
||||||
import { WebView, WebViewMessageEvent } from 'react-native-webview';
|
import { WebView, WebViewMessageEvent } from 'react-native-webview';
|
||||||
import { SafeAreaView, Text, Platform, Appearance } from 'react-native';
|
import { SafeAreaView, Text, Platform, Appearance, DeviceEventEmitter } from 'react-native';
|
||||||
import TorClient from './services/Tor';
|
import TorClient from './services/Tor';
|
||||||
import Clipboard from '@react-native-clipboard/clipboard';
|
import Clipboard from '@react-native-clipboard/clipboard';
|
||||||
import NetInfo from '@react-native-community/netinfo';
|
|
||||||
import EncryptedStorage from 'react-native-encrypted-storage';
|
import EncryptedStorage from 'react-native-encrypted-storage';
|
||||||
import { name as app_name, version as app_version } from './package.json';
|
import { name as app_name, version as app_version } from './package.json';
|
||||||
|
import TorModule from './native/TorModule';
|
||||||
|
import RoboIdentitiesModule from './native/RoboIdentitiesModule';
|
||||||
|
|
||||||
const backgroundColors = {
|
const backgroundColors = {
|
||||||
light: 'white',
|
light: 'white',
|
||||||
dark: 'black',
|
dark: 'black',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type TorStatus = 'ON' | 'STARTING' | 'STOPPING' | 'OFF';
|
||||||
|
|
||||||
const App = () => {
|
const App = () => {
|
||||||
const colorScheme = Appearance.getColorScheme() ?? 'light';
|
const colorScheme = Appearance.getColorScheme() ?? 'light';
|
||||||
const torClient = new TorClient();
|
const torClient = new TorClient();
|
||||||
const webViewRef = useRef<WebView>();
|
const webViewRef = useRef<WebView>();
|
||||||
const uri = (Platform.OS === 'android' ? 'file:///android_asset/' : '') + 'Web.bundle/index.html';
|
const uri = (Platform.OS === 'android' ? 'file:///android_asset/' : '') + 'Web.bundle/index.html';
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
TorModule.start();
|
||||||
|
DeviceEventEmitter.addListener('TorStatus', (payload) => {
|
||||||
|
if (payload.torStatus === 'OFF') TorModule.restart();
|
||||||
|
injectMessage({
|
||||||
|
category: 'system',
|
||||||
|
type: 'torStatus',
|
||||||
|
detail: payload.torStatus,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
TorModule.getTorStatus();
|
||||||
|
}, 2000);
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}, []);
|
||||||
|
|
||||||
const injectMessageResolve = (id: string, data?: object) => {
|
const injectMessageResolve = (id: string, data?: object) => {
|
||||||
const json = JSON.stringify(data || {});
|
const json = JSON.stringify(data || {});
|
||||||
webViewRef.current?.injectJavaScript(
|
webViewRef.current?.injectJavaScript(
|
||||||
@ -49,6 +71,7 @@ const App = () => {
|
|||||||
loadCookie('settings_mode');
|
loadCookie('settings_mode');
|
||||||
loadCookie('settings_light_qr');
|
loadCookie('settings_light_qr');
|
||||||
loadCookie('settings_network');
|
loadCookie('settings_network');
|
||||||
|
loadCookie('settings_use_proxy');
|
||||||
loadCookie('garage_slots').then(() => injectMessageResolve(responseId));
|
loadCookie('garage_slots').then(() => injectMessageResolve(responseId));
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -72,7 +95,7 @@ const App = () => {
|
|||||||
const onMessage = async (event: WebViewMessageEvent) => {
|
const onMessage = async (event: WebViewMessageEvent) => {
|
||||||
const data = JSON.parse(event.nativeEvent.data);
|
const data = JSON.parse(event.nativeEvent.data);
|
||||||
if (data.category === 'http') {
|
if (data.category === 'http') {
|
||||||
sendTorStatus();
|
TorModule.getTorStatus();
|
||||||
if (data.type === 'get') {
|
if (data.type === 'get') {
|
||||||
torClient
|
torClient
|
||||||
.get(data.baseUrl, data.path, data.headers)
|
.get(data.baseUrl, data.path, data.headers)
|
||||||
@ -80,7 +103,7 @@ const App = () => {
|
|||||||
injectMessageResolve(data.id, response);
|
injectMessageResolve(data.id, response);
|
||||||
})
|
})
|
||||||
.catch((e) => onCatch(data.id, e))
|
.catch((e) => onCatch(data.id, e))
|
||||||
.finally(sendTorStatus);
|
.finally(TorModule.getTorStatus);
|
||||||
} else if (data.type === 'post') {
|
} else if (data.type === 'post') {
|
||||||
torClient
|
torClient
|
||||||
.post(data.baseUrl, data.path, data.body, data.headers)
|
.post(data.baseUrl, data.path, data.body, data.headers)
|
||||||
@ -88,7 +111,7 @@ const App = () => {
|
|||||||
injectMessageResolve(data.id, response);
|
injectMessageResolve(data.id, response);
|
||||||
})
|
})
|
||||||
.catch((e) => onCatch(data.id, e))
|
.catch((e) => onCatch(data.id, e))
|
||||||
.finally(sendTorStatus);
|
.finally(TorModule.getTorStatus);
|
||||||
} else if (data.type === 'delete') {
|
} else if (data.type === 'delete') {
|
||||||
torClient
|
torClient
|
||||||
.delete(data.baseUrl, data.path, data.headers)
|
.delete(data.baseUrl, data.path, data.headers)
|
||||||
@ -96,15 +119,7 @@ const App = () => {
|
|||||||
injectMessageResolve(data.id, response);
|
injectMessageResolve(data.id, response);
|
||||||
})
|
})
|
||||||
.catch((e) => onCatch(data.id, e))
|
.catch((e) => onCatch(data.id, e))
|
||||||
.finally(sendTorStatus);
|
.finally(TorModule.getTorStatus);
|
||||||
} else if (data.type === 'xhr') {
|
|
||||||
torClient
|
|
||||||
.request(data.baseUrl, data.path)
|
|
||||||
.then((response: object) => {
|
|
||||||
injectMessageResolve(data.id, response);
|
|
||||||
})
|
|
||||||
.catch((e) => onCatch(data.id, e))
|
|
||||||
.finally(sendTorStatus);
|
|
||||||
}
|
}
|
||||||
} else if (data.category === 'system') {
|
} else if (data.category === 'system') {
|
||||||
if (data.type === 'init') {
|
if (data.type === 'init') {
|
||||||
@ -116,6 +131,14 @@ const App = () => {
|
|||||||
} else if (data.type === 'deleteCookie') {
|
} else if (data.type === 'deleteCookie') {
|
||||||
EncryptedStorage.removeItem(data.key);
|
EncryptedStorage.removeItem(data.key);
|
||||||
}
|
}
|
||||||
|
} else if (data.category === 'roboidentities') {
|
||||||
|
if (data.type === 'roboname') {
|
||||||
|
const roboname = await RoboIdentitiesModule.generateRoboname(data.detail);
|
||||||
|
injectMessageResolve(data.id, { roboname });
|
||||||
|
} else if (data.type === 'robohash') {
|
||||||
|
const robohash = await RoboIdentitiesModule.generateRobohash(data.detail);
|
||||||
|
injectMessageResolve(data.id, { robohash });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -132,23 +155,6 @@ const App = () => {
|
|||||||
} catch (error) {}
|
} catch (error) {}
|
||||||
};
|
};
|
||||||
|
|
||||||
const sendTorStatus = async (event?: any) => {
|
|
||||||
NetInfo.fetch().then(async (state) => {
|
|
||||||
let daemonStatus = 'ERROR';
|
|
||||||
if (state.isInternetReachable) {
|
|
||||||
try {
|
|
||||||
daemonStatus = await torClient.daemon.getDaemonStatus();
|
|
||||||
} catch {}
|
|
||||||
}
|
|
||||||
|
|
||||||
injectMessage({
|
|
||||||
category: 'system',
|
|
||||||
type: 'torStatus',
|
|
||||||
detail: daemonStatus,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeAreaView style={{ flex: 1, backgroundColor: backgroundColors[colorScheme] }}>
|
<SafeAreaView style={{ flex: 1, backgroundColor: backgroundColors[colorScheme] }}>
|
||||||
<WebView
|
<WebView
|
||||||
|
@ -151,8 +151,8 @@ android {
|
|||||||
applicationId "com.robosats"
|
applicationId "com.robosats"
|
||||||
minSdkVersion rootProject.ext.minSdkVersion
|
minSdkVersion rootProject.ext.minSdkVersion
|
||||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||||
versionCode 1
|
versionCode 2
|
||||||
versionName "0.6.0-alpha"
|
versionName "0.6.2-alpha"
|
||||||
buildConfigField "boolean", "IS_NEW_ARCHITECTURE_ENABLED", isNewArchitectureEnabled().toString()
|
buildConfigField "boolean", "IS_NEW_ARCHITECTURE_ENABLED", isNewArchitectureEnabled().toString()
|
||||||
|
|
||||||
if (isNewArchitectureEnabled()) {
|
if (isNewArchitectureEnabled()) {
|
||||||
@ -238,6 +238,12 @@ android {
|
|||||||
keyAlias 'androiddebugkey'
|
keyAlias 'androiddebugkey'
|
||||||
keyPassword 'android'
|
keyPassword 'android'
|
||||||
}
|
}
|
||||||
|
release {
|
||||||
|
storeFile file("../../../keystore.jks")
|
||||||
|
keyAlias System.getenv("KEY_ALIAS")
|
||||||
|
storePassword System.getenv("KEY_STORE_PASS")
|
||||||
|
keyPassword System.getenv("KEY_PASS")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
buildTypes {
|
buildTypes {
|
||||||
debug {
|
debug {
|
||||||
@ -246,7 +252,7 @@ android {
|
|||||||
release {
|
release {
|
||||||
// Caution! In production, you need to generate your own keystore file.
|
// Caution! In production, you need to generate your own keystore file.
|
||||||
// see https://reactnative.dev/docs/signed-apk-android.
|
// see https://reactnative.dev/docs/signed-apk-android.
|
||||||
signingConfig signingConfigs.debug
|
signingConfig signingConfigs.release
|
||||||
minifyEnabled enableProguardInReleaseBuilds
|
minifyEnabled enableProguardInReleaseBuilds
|
||||||
proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
|
proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
|
||||||
}
|
}
|
||||||
@ -271,6 +277,14 @@ android {
|
|||||||
packagingOptions {
|
packagingOptions {
|
||||||
// Make sure libjsc.so does not packed in APK
|
// Make sure libjsc.so does not packed in APK
|
||||||
exclude "**/libjsc.so"
|
exclude "**/libjsc.so"
|
||||||
|
jniLibs.useLegacyPackaging = true
|
||||||
|
}
|
||||||
|
|
||||||
|
dependenciesInfo {
|
||||||
|
// Disables dependency metadata when building APKs.
|
||||||
|
includeInApk = false
|
||||||
|
// Disables dependency metadata when building Android App Bundles.
|
||||||
|
includeInBundle = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -282,7 +296,9 @@ dependencies {
|
|||||||
|
|
||||||
implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.0.0"
|
implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.0.0"
|
||||||
|
|
||||||
implementation files("../../node_modules/react-native-tor/android/libs/sifir_android.aar")
|
implementation "io.matthewnelson.kotlin-components:kmp-tor:4.8.6-0-1.4.4"
|
||||||
|
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.2"
|
||||||
|
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2"
|
||||||
|
|
||||||
if (enableHermes) {
|
if (enableHermes) {
|
||||||
//noinspection GradleDynamicVersion
|
//noinspection GradleDynamicVersion
|
||||||
@ -326,3 +342,5 @@ def isNewArchitectureEnabled() {
|
|||||||
// - Set an environment variable `ORG_GRADLE_PROJECT_newArchEnabled=true`
|
// - Set an environment variable `ORG_GRADLE_PROJECT_newArchEnabled=true`
|
||||||
return project.hasProperty("newArchEnabled") && project.newArchEnabled == "true"
|
return project.hasProperty("newArchEnabled") && project.newArchEnabled == "true"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
apply plugin: 'kotlin-android'
|
||||||
|
@ -10,7 +10,9 @@
|
|||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
android:allowBackup="false"
|
android:allowBackup="false"
|
||||||
android:usesCleartextTraffic="true"
|
android:usesCleartextTraffic="true"
|
||||||
android:theme="@style/AppTheme">
|
android:theme="@style/AppTheme"
|
||||||
|
android:extractNativeLibs="true"
|
||||||
|
>
|
||||||
<activity
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
|
@ -1,17 +1,14 @@
|
|||||||
package com.robosats;
|
package com.robosats;
|
||||||
|
|
||||||
import android.app.Application;
|
import android.app.Application;
|
||||||
import android.content.Context;
|
|
||||||
import com.facebook.react.PackageList;
|
import com.facebook.react.PackageList;
|
||||||
import com.facebook.react.ReactApplication;
|
import com.facebook.react.ReactApplication;
|
||||||
import com.facebook.react.ReactInstanceManager;
|
|
||||||
import com.facebook.react.ReactNativeHost;
|
import com.facebook.react.ReactNativeHost;
|
||||||
import com.facebook.react.ReactPackage;
|
import com.facebook.react.ReactPackage;
|
||||||
import com.facebook.react.config.ReactFeatureFlags;
|
import com.facebook.react.config.ReactFeatureFlags;
|
||||||
import com.facebook.soloader.SoLoader;
|
import com.facebook.soloader.SoLoader;
|
||||||
import android.webkit.WebView;
|
import android.webkit.WebView;
|
||||||
import com.robosats.newarchitecture.MainApplicationReactNativeHost;
|
import com.robosats.newarchitecture.MainApplicationReactNativeHost;
|
||||||
import java.lang.reflect.InvocationTargetException;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
public class MainApplication extends Application implements ReactApplication {
|
public class MainApplication extends Application implements ReactApplication {
|
||||||
@ -29,6 +26,8 @@ public class MainApplication extends Application implements ReactApplication {
|
|||||||
List<ReactPackage> packages = new PackageList(this).getPackages();
|
List<ReactPackage> packages = new PackageList(this).getPackages();
|
||||||
// Packages that cannot be autolinked yet can be added manually here, for example:
|
// Packages that cannot be autolinked yet can be added manually here, for example:
|
||||||
// packages.add(new MyReactNativePackage());
|
// packages.add(new MyReactNativePackage());
|
||||||
|
packages.add(new RobosatsPackage());
|
||||||
|
|
||||||
return packages;
|
return packages;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -0,0 +1,22 @@
|
|||||||
|
package com.robosats;
|
||||||
|
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
public class RoboIdentities {
|
||||||
|
static {
|
||||||
|
System.loadLibrary("robonames");
|
||||||
|
System.loadLibrary("robohash");
|
||||||
|
}
|
||||||
|
|
||||||
|
public String generateRoboname(String initial_string) {
|
||||||
|
return nativeGenerateRoboname(initial_string);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String generateRobohash(String initial_string) {
|
||||||
|
return nativeGenerateRobohash(initial_string);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Native functions implemented in Rust.
|
||||||
|
private static native String nativeGenerateRoboname(String initial_string);
|
||||||
|
private static native String nativeGenerateRobohash(String initial_string);
|
||||||
|
}
|
@ -0,0 +1,30 @@
|
|||||||
|
package com.robosats;
|
||||||
|
|
||||||
|
import com.facebook.react.ReactPackage;
|
||||||
|
import com.facebook.react.bridge.NativeModule;
|
||||||
|
import com.facebook.react.bridge.ReactApplicationContext;
|
||||||
|
import com.facebook.react.uimanager.ViewManager;
|
||||||
|
import com.robosats.modules.RoboIdentitiesModule;
|
||||||
|
import com.robosats.modules.TorModule;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class RobosatsPackage implements ReactPackage {
|
||||||
|
@Override
|
||||||
|
public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<NativeModule> createNativeModules(
|
||||||
|
ReactApplicationContext reactContext) {
|
||||||
|
List<NativeModule> modules = new ArrayList<>();
|
||||||
|
|
||||||
|
modules.add(new TorModule(reactContext));
|
||||||
|
modules.add(new RoboIdentitiesModule(reactContext));
|
||||||
|
|
||||||
|
return modules;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,37 @@
|
|||||||
|
package com.robosats.modules;
|
||||||
|
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
import com.facebook.react.bridge.Arguments;
|
||||||
|
import com.facebook.react.bridge.Promise;
|
||||||
|
import com.facebook.react.bridge.ReactApplicationContext;
|
||||||
|
import com.facebook.react.bridge.ReactContextBaseJavaModule;
|
||||||
|
import com.facebook.react.bridge.ReactMethod;
|
||||||
|
import com.facebook.react.bridge.WritableMap;
|
||||||
|
import com.facebook.react.modules.core.DeviceEventManagerModule;
|
||||||
|
import com.robosats.RoboIdentities;
|
||||||
|
|
||||||
|
public class RoboIdentitiesModule extends ReactContextBaseJavaModule {
|
||||||
|
private ReactApplicationContext context;
|
||||||
|
|
||||||
|
public RoboIdentitiesModule(ReactApplicationContext reactContext) {
|
||||||
|
context = reactContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return "RoboIdentitiesModule";
|
||||||
|
}
|
||||||
|
|
||||||
|
@ReactMethod
|
||||||
|
public void generateRoboname(String initial_string, final Promise promise) {
|
||||||
|
String roboname = new RoboIdentities().generateRoboname(initial_string);
|
||||||
|
promise.resolve(roboname);
|
||||||
|
}
|
||||||
|
|
||||||
|
@ReactMethod
|
||||||
|
public void generateRobohash(String initial_string, final Promise promise) {
|
||||||
|
String robohash = new RoboIdentities().generateRobohash(initial_string);
|
||||||
|
promise.resolve(robohash);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,158 @@
|
|||||||
|
package com.robosats.modules;
|
||||||
|
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
import com.facebook.react.bridge.Arguments;
|
||||||
|
import com.facebook.react.bridge.Promise;
|
||||||
|
import com.facebook.react.bridge.ReactApplicationContext;
|
||||||
|
import com.facebook.react.bridge.ReactContextBaseJavaModule;
|
||||||
|
import com.facebook.react.bridge.ReactMethod;
|
||||||
|
import com.facebook.react.bridge.WritableMap;
|
||||||
|
import com.facebook.react.modules.core.DeviceEventManagerModule;
|
||||||
|
import com.robosats.tor.TorKmpManager;
|
||||||
|
|
||||||
|
import org.json.JSONException;
|
||||||
|
import org.json.JSONObject;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
import okhttp3.Call;
|
||||||
|
import okhttp3.Callback;
|
||||||
|
import okhttp3.MediaType;
|
||||||
|
import okhttp3.OkHttpClient;
|
||||||
|
import okhttp3.Request;
|
||||||
|
import okhttp3.RequestBody;
|
||||||
|
import okhttp3.Response;
|
||||||
|
|
||||||
|
|
||||||
|
public class TorModule extends ReactContextBaseJavaModule {
|
||||||
|
private TorKmpManager torKmpManager;
|
||||||
|
private ReactApplicationContext context;
|
||||||
|
public TorModule(ReactApplicationContext reactContext) {
|
||||||
|
context = reactContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return "TorModule";
|
||||||
|
}
|
||||||
|
|
||||||
|
@ReactMethod
|
||||||
|
public void sendRequest(String action, String url, String headers, String body, final Promise promise) throws JSONException {
|
||||||
|
OkHttpClient client = new OkHttpClient.Builder()
|
||||||
|
.connectTimeout(60, TimeUnit.SECONDS) // Set connection timeout
|
||||||
|
.readTimeout(30, TimeUnit.SECONDS) // Set read timeout
|
||||||
|
.proxy(torKmpManager.getProxy()).build();
|
||||||
|
|
||||||
|
Request.Builder requestBuilder = new Request.Builder().url(url);
|
||||||
|
|
||||||
|
JSONObject headersObject = new JSONObject(headers);
|
||||||
|
headersObject.keys().forEachRemaining(key -> {
|
||||||
|
String value = headersObject.optString(key);
|
||||||
|
requestBuilder.addHeader(key, value);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (Objects.equals(action, "DELETE")) {
|
||||||
|
requestBuilder.delete();
|
||||||
|
} else if (Objects.equals(action, "POST")) {
|
||||||
|
RequestBody requestBody = RequestBody.create(body, MediaType.get("application/json; charset=utf-8"));
|
||||||
|
requestBuilder.post(requestBody);
|
||||||
|
} else {
|
||||||
|
requestBuilder.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
Request request = requestBuilder.build();
|
||||||
|
client.newCall(request).enqueue(new Callback() {
|
||||||
|
@Override
|
||||||
|
public void onFailure(@NonNull Call call, @NonNull IOException e) {
|
||||||
|
Log.d("RobosatsError", e.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onResponse(Call call, Response response) throws IOException {
|
||||||
|
String body = response.body() != null ? response.body().string() : "{}";
|
||||||
|
JSONObject headersJson = new JSONObject();
|
||||||
|
response.headers().names().forEach(name -> {
|
||||||
|
try {
|
||||||
|
headersJson.put(name, response.header(name));
|
||||||
|
} catch (JSONException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
promise.resolve("{\"json\":" + body + ", \"headers\": " + headersJson +"}");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@ReactMethod
|
||||||
|
public void getTorStatus() {
|
||||||
|
String torState = torKmpManager.getTorState().getState().name();
|
||||||
|
WritableMap payload = Arguments.createMap();
|
||||||
|
payload.putString("torStatus", torState);
|
||||||
|
context
|
||||||
|
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
|
||||||
|
.emit("TorStatus", payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
@ReactMethod
|
||||||
|
public void isConnected() {
|
||||||
|
String isConnected = String.valueOf(torKmpManager.isConnected());
|
||||||
|
WritableMap payload = Arguments.createMap();
|
||||||
|
payload.putString("isConnected", isConnected);
|
||||||
|
context
|
||||||
|
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
|
||||||
|
.emit("TorIsConnected", payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
@ReactMethod
|
||||||
|
public void isStarting() {
|
||||||
|
String isStarting = String.valueOf(torKmpManager.isStarting());
|
||||||
|
WritableMap payload = Arguments.createMap();
|
||||||
|
payload.putString("isStarting", isStarting);
|
||||||
|
context
|
||||||
|
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
|
||||||
|
.emit("TorIsStarting", payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
@ReactMethod
|
||||||
|
public void stop() {
|
||||||
|
torKmpManager.getTorOperationManager().stopQuietly();
|
||||||
|
WritableMap payload = Arguments.createMap();
|
||||||
|
context
|
||||||
|
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
|
||||||
|
.emit("TorStop", payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
@ReactMethod
|
||||||
|
public void start() {
|
||||||
|
torKmpManager = new TorKmpManager(context.getCurrentActivity().getApplication());
|
||||||
|
torKmpManager.getTorOperationManager().startQuietly();
|
||||||
|
WritableMap payload = Arguments.createMap();
|
||||||
|
context
|
||||||
|
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
|
||||||
|
.emit("TorStart", payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
@ReactMethod
|
||||||
|
public void restart() {
|
||||||
|
torKmpManager = new TorKmpManager(context.getCurrentActivity().getApplication());
|
||||||
|
torKmpManager.getTorOperationManager().restartQuietly();
|
||||||
|
WritableMap payload = Arguments.createMap();
|
||||||
|
context
|
||||||
|
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
|
||||||
|
.emit("TorRestart", payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
@ReactMethod
|
||||||
|
public void newIdentity() {
|
||||||
|
torKmpManager.newIdentity(context.getCurrentActivity().getApplication());
|
||||||
|
WritableMap payload = Arguments.createMap();
|
||||||
|
context
|
||||||
|
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
|
||||||
|
.emit("TorNewIdentity", payload);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,8 @@
|
|||||||
|
package com.robosats.tor
|
||||||
|
|
||||||
|
enum class EnumTorState {
|
||||||
|
STARTING,
|
||||||
|
ON,
|
||||||
|
STOPPING,
|
||||||
|
OFF
|
||||||
|
}
|
@ -0,0 +1,389 @@
|
|||||||
|
package com.robosats.tor
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import android.util.Log
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.lifecycle.LiveData
|
||||||
|
import androidx.lifecycle.MutableLiveData
|
||||||
|
import io.matthewnelson.kmp.tor.KmpTorLoaderAndroid
|
||||||
|
import io.matthewnelson.kmp.tor.TorConfigProviderAndroid
|
||||||
|
import io.matthewnelson.kmp.tor.common.address.*
|
||||||
|
import io.matthewnelson.kmp.tor.controller.common.config.TorConfig
|
||||||
|
import io.matthewnelson.kmp.tor.controller.common.config.TorConfig.Option.*
|
||||||
|
import io.matthewnelson.kmp.tor.controller.common.config.TorConfig.Setting.*
|
||||||
|
import io.matthewnelson.kmp.tor.controller.common.control.usecase.TorControlInfoGet
|
||||||
|
import io.matthewnelson.kmp.tor.controller.common.control.usecase.TorControlSignal
|
||||||
|
import io.matthewnelson.kmp.tor.controller.common.events.TorEvent
|
||||||
|
import io.matthewnelson.kmp.tor.manager.TorManager
|
||||||
|
import io.matthewnelson.kmp.tor.manager.TorServiceConfig
|
||||||
|
import io.matthewnelson.kmp.tor.manager.common.TorControlManager
|
||||||
|
import io.matthewnelson.kmp.tor.manager.common.TorOperationManager
|
||||||
|
import io.matthewnelson.kmp.tor.manager.common.event.TorManagerEvent
|
||||||
|
import io.matthewnelson.kmp.tor.manager.common.state.isOff
|
||||||
|
import io.matthewnelson.kmp.tor.manager.common.state.isOn
|
||||||
|
import io.matthewnelson.kmp.tor.manager.common.state.isStarting
|
||||||
|
import io.matthewnelson.kmp.tor.manager.common.state.isStopping
|
||||||
|
import io.matthewnelson.kmp.tor.manager.R
|
||||||
|
import kotlinx.coroutines.*
|
||||||
|
import java.net.InetSocketAddress
|
||||||
|
import java.net.Proxy
|
||||||
|
|
||||||
|
class TorKmpManager(application : Application) {
|
||||||
|
|
||||||
|
private val TAG = "TorListener"
|
||||||
|
|
||||||
|
private val providerAndroid by lazy {
|
||||||
|
object : TorConfigProviderAndroid(context = application) {
|
||||||
|
override fun provide(): TorConfig {
|
||||||
|
return TorConfig.Builder {
|
||||||
|
// Set multiple ports for all of the things
|
||||||
|
val dns = Ports.Dns()
|
||||||
|
put(dns.set(AorDorPort.Value(PortProxy(9252))))
|
||||||
|
put(dns.set(AorDorPort.Value(PortProxy(9253))))
|
||||||
|
|
||||||
|
val socks = Ports.Socks()
|
||||||
|
put(socks.set(AorDorPort.Value(PortProxy(9254))))
|
||||||
|
put(socks.set(AorDorPort.Value(PortProxy(9255))))
|
||||||
|
|
||||||
|
val http = Ports.HttpTunnel()
|
||||||
|
put(http.set(AorDorPort.Value(PortProxy(9258))))
|
||||||
|
put(http.set(AorDorPort.Value(PortProxy(9259))))
|
||||||
|
|
||||||
|
val trans = Ports.Trans()
|
||||||
|
put(trans.set(AorDorPort.Value(PortProxy(9262))))
|
||||||
|
put(trans.set(AorDorPort.Value(PortProxy(9263))))
|
||||||
|
|
||||||
|
// If a port (9263) is already taken (by ^^^^ trans port above)
|
||||||
|
// this will take its place and "overwrite" the trans port entry
|
||||||
|
// because port 9263 is taken.
|
||||||
|
put(socks.set(AorDorPort.Value(PortProxy(9263))))
|
||||||
|
|
||||||
|
// Set Flags
|
||||||
|
socks.setFlags(setOf(
|
||||||
|
Ports.Socks.Flag.OnionTrafficOnly
|
||||||
|
)).setIsolationFlags(setOf(
|
||||||
|
Ports.IsolationFlag.IsolateClientAddr,
|
||||||
|
)).set(AorDorPort.Value(PortProxy(9264)))
|
||||||
|
put(socks)
|
||||||
|
|
||||||
|
// reset our socks object to defaults
|
||||||
|
socks.setDefault()
|
||||||
|
|
||||||
|
// Not necessary, as if ControlPort is missing it will be
|
||||||
|
// automatically added for you; but for demonstration purposes...
|
||||||
|
// put(Ports.Control().set(AorDorPort.Auto))
|
||||||
|
|
||||||
|
// Use a UnixSocket instead of TCP for the ControlPort.
|
||||||
|
//
|
||||||
|
// A unix domain socket will always be preferred on Android
|
||||||
|
// if neither Ports.Control or UnixSockets.Control are provided.
|
||||||
|
put(UnixSockets.Control().set(FileSystemFile(
|
||||||
|
workDir.builder {
|
||||||
|
|
||||||
|
// Put the file in the "data" directory
|
||||||
|
// so that we avoid any directory permission
|
||||||
|
// issues.
|
||||||
|
//
|
||||||
|
// Note that DataDirectory is automatically added
|
||||||
|
// for you if it is not present in your provided
|
||||||
|
// config. If you set a custom Path for it, you
|
||||||
|
// should use it here.
|
||||||
|
addSegment(DataDirectory.DEFAULT_NAME)
|
||||||
|
|
||||||
|
addSegment(UnixSockets.Control.DEFAULT_NAME)
|
||||||
|
}
|
||||||
|
)))
|
||||||
|
|
||||||
|
// Use a UnixSocket instead of TCP for the SocksPort.
|
||||||
|
put(UnixSockets.Socks().set(FileSystemFile(
|
||||||
|
workDir.builder {
|
||||||
|
|
||||||
|
// Put the file in the "data" directory
|
||||||
|
// so that we avoid any directory permission
|
||||||
|
// issues.
|
||||||
|
//
|
||||||
|
// Note that DataDirectory is automatically added
|
||||||
|
// for you if it is not present in your provided
|
||||||
|
// config. If you set a custom Path for it, you
|
||||||
|
// should use it here.
|
||||||
|
addSegment(DataDirectory.DEFAULT_NAME)
|
||||||
|
|
||||||
|
addSegment(UnixSockets.Socks.DEFAULT_NAME)
|
||||||
|
}
|
||||||
|
)))
|
||||||
|
|
||||||
|
// For Android, disabling & reducing connection padding is
|
||||||
|
// advisable to minimize mobile data usage.
|
||||||
|
put(ConnectionPadding().set(AorTorF.False))
|
||||||
|
put(ConnectionPaddingReduced().set(TorF.True))
|
||||||
|
|
||||||
|
// Tor default is 24h. Reducing to 10 min helps mitigate
|
||||||
|
// unnecessary mobile data usage.
|
||||||
|
put(DormantClientTimeout().set(Time.Minutes(10)))
|
||||||
|
|
||||||
|
// Tor defaults this setting to false which would mean if
|
||||||
|
// Tor goes dormant, the next time it is started it will still
|
||||||
|
// be in the dormant state and will not bootstrap until being
|
||||||
|
// set to "active". This ensures that if it is a fresh start,
|
||||||
|
// dormancy will be cancelled automatically.
|
||||||
|
put(DormantCanceledByStartup().set(TorF.True))
|
||||||
|
|
||||||
|
// If planning to use v3 Client Authentication in a persistent
|
||||||
|
// manner (where private keys are saved to disk via the "Persist"
|
||||||
|
// flag), this is needed to be set.
|
||||||
|
put(ClientOnionAuthDir().set(FileSystemDir(
|
||||||
|
workDir.builder { addSegment(ClientOnionAuthDir.DEFAULT_NAME) }
|
||||||
|
)))
|
||||||
|
|
||||||
|
val hsPath = workDir.builder {
|
||||||
|
addSegment(HiddenService.DEFAULT_PARENT_DIR_NAME)
|
||||||
|
addSegment("test_service")
|
||||||
|
}
|
||||||
|
// Add Hidden services
|
||||||
|
put(HiddenService()
|
||||||
|
.setPorts(ports = setOf(
|
||||||
|
// Use a unix domain socket to communicate via IPC instead of over TCP
|
||||||
|
HiddenService.UnixSocket(virtualPort = Port(80), targetUnixSocket = hsPath.builder {
|
||||||
|
addSegment(HiddenService.UnixSocket.DEFAULT_UNIX_SOCKET_NAME)
|
||||||
|
}),
|
||||||
|
))
|
||||||
|
.setMaxStreams(maxStreams = HiddenService.MaxStreams(value = 2))
|
||||||
|
.setMaxStreamsCloseCircuit(value = TorF.True)
|
||||||
|
.set(FileSystemDir(path = hsPath))
|
||||||
|
)
|
||||||
|
|
||||||
|
put(HiddenService()
|
||||||
|
.setPorts(ports = setOf(
|
||||||
|
HiddenService.Ports(virtualPort = Port(80), targetPort = Port(1030)), // http
|
||||||
|
HiddenService.Ports(virtualPort = Port(443), targetPort = Port(1030)) // https
|
||||||
|
))
|
||||||
|
.set(FileSystemDir(path =
|
||||||
|
workDir.builder {
|
||||||
|
addSegment(HiddenService.DEFAULT_PARENT_DIR_NAME)
|
||||||
|
addSegment("test_service_2")
|
||||||
|
}
|
||||||
|
))
|
||||||
|
)
|
||||||
|
}.build()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val loaderAndroid by lazy {
|
||||||
|
KmpTorLoaderAndroid(provider = providerAndroid)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val manager: TorManager by lazy {
|
||||||
|
TorManager.newInstance(application = application, loader = loaderAndroid, requiredEvents = null)
|
||||||
|
}
|
||||||
|
|
||||||
|
// only expose necessary interfaces
|
||||||
|
val torOperationManager: TorOperationManager get() = manager
|
||||||
|
val torControlManager: TorControlManager get() = manager
|
||||||
|
|
||||||
|
private val listener = TorListener()
|
||||||
|
|
||||||
|
val events: LiveData<String> get() = listener.eventLines
|
||||||
|
|
||||||
|
private val appScope by lazy {
|
||||||
|
CoroutineScope(Dispatchers.Main.immediate + SupervisorJob())
|
||||||
|
}
|
||||||
|
|
||||||
|
val torStateLiveData: MutableLiveData<TorState> = MutableLiveData()
|
||||||
|
get() = field
|
||||||
|
var torState: TorState = TorState()
|
||||||
|
get() = field
|
||||||
|
|
||||||
|
var proxy: Proxy? = null
|
||||||
|
get() = field
|
||||||
|
|
||||||
|
init {
|
||||||
|
manager.debug(true)
|
||||||
|
manager.addListener(listener)
|
||||||
|
listener.addLine(TorServiceConfig.getMetaData(application).toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isConnected(): Boolean {
|
||||||
|
return manager.state.isOn() && manager.state.bootstrap >= 100
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isStarting(): Boolean {
|
||||||
|
return manager.state.isStarting() ||
|
||||||
|
(manager.state.isOn() && manager.state.bootstrap < 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun newIdentity(appContext: Application) {
|
||||||
|
appScope.launch {
|
||||||
|
val result = manager.signal(TorControlSignal.Signal.NewNym)
|
||||||
|
result.onSuccess {
|
||||||
|
if (it !is String) {
|
||||||
|
listener.addLine(TorControlSignal.NEW_NYM_SUCCESS)
|
||||||
|
Toast.makeText(appContext, TorControlSignal.NEW_NYM_SUCCESS, Toast.LENGTH_SHORT).show()
|
||||||
|
return@onSuccess
|
||||||
|
}
|
||||||
|
|
||||||
|
val post: String? = when {
|
||||||
|
it.startsWith(TorControlSignal.NEW_NYM_RATE_LIMITED) -> {
|
||||||
|
// Rate limiting NEWNYM request: delaying by 8 second(s)
|
||||||
|
val seconds: Int? = it.drop(TorControlSignal.NEW_NYM_RATE_LIMITED.length)
|
||||||
|
.substringBefore(' ')
|
||||||
|
.toIntOrNull()
|
||||||
|
|
||||||
|
if (seconds == null) {
|
||||||
|
it
|
||||||
|
} else {
|
||||||
|
appContext.getString(
|
||||||
|
R.string.kmp_tor_newnym_rate_limited,
|
||||||
|
seconds
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
it == TorControlSignal.NEW_NYM_SUCCESS -> {
|
||||||
|
appContext.getString(R.string.kmp_tor_newnym_success)
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (post != null) {
|
||||||
|
listener.addLine(post)
|
||||||
|
Toast.makeText(appContext, post, Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result.onFailure {
|
||||||
|
val msg = "Tor identity change failed"
|
||||||
|
listener.addLine(msg)
|
||||||
|
Toast.makeText(appContext, msg, Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private inner class TorListener: TorManagerEvent.Listener() {
|
||||||
|
private val _eventLines: MutableLiveData<String> = MutableLiveData("")
|
||||||
|
val eventLines: LiveData<String> = _eventLines
|
||||||
|
private val events: MutableList<String> = ArrayList(50)
|
||||||
|
fun addLine(line: String) {
|
||||||
|
synchronized(this) {
|
||||||
|
if (events.size > 49) {
|
||||||
|
events.removeAt(0)
|
||||||
|
}
|
||||||
|
events.add(line)
|
||||||
|
//Log.i(TAG, line)
|
||||||
|
//_eventLines.value = events.joinToString("\n")
|
||||||
|
_eventLines.postValue(events.joinToString("\n"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onEvent(event: TorManagerEvent) {
|
||||||
|
|
||||||
|
if (event is TorManagerEvent.State) {
|
||||||
|
val stateEvent: TorManagerEvent.State = event
|
||||||
|
val state = stateEvent.torState
|
||||||
|
torState.progressIndicator = state.bootstrap
|
||||||
|
val liveTorState = TorState()
|
||||||
|
liveTorState.progressIndicator = state.bootstrap
|
||||||
|
|
||||||
|
if (state.isOn()) {
|
||||||
|
if (state.bootstrap >= 100) {
|
||||||
|
torState.state = EnumTorState.ON
|
||||||
|
liveTorState.state = EnumTorState.ON
|
||||||
|
} else {
|
||||||
|
torState.state = EnumTorState.STARTING
|
||||||
|
liveTorState.state = EnumTorState.STARTING
|
||||||
|
}
|
||||||
|
} else if (state.isStarting()) {
|
||||||
|
torState.state = EnumTorState.STARTING
|
||||||
|
liveTorState.state = EnumTorState.STARTING
|
||||||
|
} else if (state.isOff()) {
|
||||||
|
torState.state = EnumTorState.OFF
|
||||||
|
liveTorState.state = EnumTorState.OFF
|
||||||
|
} else if (state.isStopping()) {
|
||||||
|
torState.state = EnumTorState.STOPPING
|
||||||
|
liveTorState.state = EnumTorState.STOPPING
|
||||||
|
}
|
||||||
|
torStateLiveData.postValue(liveTorState)
|
||||||
|
}
|
||||||
|
addLine(event.toString())
|
||||||
|
super.onEvent(event)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onEvent(event: TorEvent.Type.SingleLineEvent, output: String) {
|
||||||
|
addLine("$event - $output")
|
||||||
|
|
||||||
|
super.onEvent(event, output)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onEvent(event: TorEvent.Type.MultiLineEvent, output: List<String>) {
|
||||||
|
addLine("multi-line event: $event. See Logs.")
|
||||||
|
|
||||||
|
// these events are many many many lines and should be moved
|
||||||
|
// off the main thread if ever needed to be dealt with.
|
||||||
|
val enabled = false
|
||||||
|
if (enabled) {
|
||||||
|
appScope.launch(Dispatchers.IO) {
|
||||||
|
Log.d(TAG, "-------------- multi-line event START: $event --------------")
|
||||||
|
for (line in output) {
|
||||||
|
Log.d(TAG, line)
|
||||||
|
}
|
||||||
|
Log.d(TAG, "--------------- multi-line event END: $event ---------------")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
super.onEvent(event, output)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun managerEventError(t: Throwable) {
|
||||||
|
t.printStackTrace()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun managerEventAddressInfo(info: TorManagerEvent.AddressInfo) {
|
||||||
|
if (info.isNull) {
|
||||||
|
// Tear down HttpClient
|
||||||
|
} else {
|
||||||
|
info.socksInfoToProxyAddressOrNull()?.firstOrNull()?.let { proxyAddress ->
|
||||||
|
@Suppress("UNUSED_VARIABLE")
|
||||||
|
val socket = InetSocketAddress(proxyAddress.address.value, proxyAddress.port.value)
|
||||||
|
proxy = Proxy(Proxy.Type.SOCKS, socket)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun managerEventStartUpCompleteForTorInstance() {
|
||||||
|
// Do one-time things after we're bootstrapped
|
||||||
|
|
||||||
|
appScope.launch {
|
||||||
|
torControlManager.onionAddNew(
|
||||||
|
type = OnionAddress.PrivateKey.Type.ED25519_V3,
|
||||||
|
hsPorts = setOf(HiddenService.Ports(virtualPort = Port(443))),
|
||||||
|
flags = null,
|
||||||
|
maxStreams = null,
|
||||||
|
).onSuccess { hsEntry ->
|
||||||
|
addLine(
|
||||||
|
"New HiddenService: " +
|
||||||
|
"\n - Address: https://${hsEntry.address.canonicalHostname()}" +
|
||||||
|
"\n - PrivateKey: ${hsEntry.privateKey}"
|
||||||
|
)
|
||||||
|
|
||||||
|
torControlManager.onionDel(hsEntry.address).onSuccess {
|
||||||
|
addLine("Aaaaaaaaand it's gone...")
|
||||||
|
}.onFailure { t ->
|
||||||
|
t.printStackTrace()
|
||||||
|
}
|
||||||
|
}.onFailure { t ->
|
||||||
|
t.printStackTrace()
|
||||||
|
}
|
||||||
|
|
||||||
|
delay(20_000L)
|
||||||
|
|
||||||
|
torControlManager.infoGet(TorControlInfoGet.KeyWord.Uptime()).onSuccess { uptime ->
|
||||||
|
addLine("Uptime - $uptime")
|
||||||
|
}.onFailure { t ->
|
||||||
|
t.printStackTrace()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,14 @@
|
|||||||
|
package com.robosats.tor
|
||||||
|
|
||||||
|
class TorState {
|
||||||
|
var state : EnumTorState = EnumTorState.OFF
|
||||||
|
get() = field
|
||||||
|
set(value) {
|
||||||
|
field = value
|
||||||
|
}
|
||||||
|
var progressIndicator : Int = 0
|
||||||
|
get() = field
|
||||||
|
set(value) {
|
||||||
|
field = value
|
||||||
|
}
|
||||||
|
}
|