Wallet improvements

This commit is contained in:
Peter 2019-09-20 19:17:04 +04:00
parent 4c26ee0654
commit 0f8c455216
67 changed files with 2636 additions and 6440 deletions

View File

@ -1053,7 +1053,7 @@ open class ListView: ASDisplayNode, UIScrollViewAccessibilityDelegate, UIGesture
if self.stackFromBottom {
topOffset = 0.0
} else {
topOffset = self.visibleSize.height
topOffset = 0.0
}
} else {
if self.stackFromBottom {

View File

@ -85,6 +85,9 @@ final class NavigationContainer: ASDisplayNode, UIGestureRecognizerDelegate {
}
}
var statusBarStyle: StatusBarStyle = .Ignore
var statusBarStyleUpdated: (() -> Void)?
init(controllerRemoved: @escaping (ViewController) -> Void) {
self.controllerRemoved = controllerRemoved
@ -286,6 +289,9 @@ final class NavigationContainer: ASDisplayNode, UIGestureRecognizerDelegate {
if let top = self.state.top {
self.applyLayout(layout: layout, to: top, transition: transition)
self.statusBarStyle = top.value.statusBar.statusBarStyle
} else {
self.statusBarStyle = .Ignore
}
if self.state.transition == nil {

View File

@ -238,6 +238,8 @@ open class NavigationController: UINavigationController, ContainableController,
let navigationLayout = makeNavigationLayout(layout: layout, controllers: self._viewControllers)
var transition = transition
var statusBarStyle: StatusBarStyle = .Ignore
var animateStatusBarStyleTransition = transition.isAnimated
var modalContainers: [NavigationModalContainer] = []
for i in 0 ..< navigationLayout.modal.count {
@ -408,6 +410,15 @@ open class NavigationController: UINavigationController, ContainableController,
}
}
if let rootContainer = self.rootContainer {
switch rootContainer {
case let .flat(container):
statusBarStyle = container.statusBarStyle
case .split:
break
}
}
switch layout.metrics.widthClass {
case .compact:
if visibleModalCount != 0 {
@ -451,16 +462,14 @@ open class NavigationController: UINavigationController, ContainableController,
rootModalFrame.updateDismissal(transition: transition, progress: effectiveRootModalDismissProgress, additionalProgress: additionalModalFrameProgress, completion: {})
}
if effectiveRootModalDismissProgress < 0.5 {
self.statusBarHost?.setStatusBarStyle(.lightContent, animated: transition.isAnimated || forceStatusBarAnimation)
} else {
let normalStatusBarStyle: UIStatusBarStyle
switch self.theme.statusBar {
case .black:
normalStatusBarStyle = .default
case .white:
normalStatusBarStyle = .lightContent
statusBarStyle = .White
if forceStatusBarAnimation {
animateStatusBarStyleTransition = true
}
} else {
if forceStatusBarAnimation {
animateStatusBarStyleTransition = true
}
self.statusBarHost?.setStatusBarStyle(normalStatusBarStyle, animated: transition.isAnimated || forceStatusBarAnimation)
}
if let rootContainer = self.rootContainer {
var rootContainerNode: ASDisplayNode
@ -494,14 +503,6 @@ open class NavigationController: UINavigationController, ContainableController,
rootModalFrame.updateDismissal(transition: transition, progress: 1.0, additionalProgress: 0.0, completion: { [weak rootModalFrame] in
rootModalFrame?.removeFromSupernode()
})
let normalStatusBarStyle: UIStatusBarStyle
switch self.theme.statusBar {
case .black:
normalStatusBarStyle = .default
case .white:
normalStatusBarStyle = .lightContent
}
self.statusBarHost?.setStatusBarStyle(normalStatusBarStyle, animated: transition.isAnimated)
}
if let rootContainer = self.rootContainer {
var rootContainerNode: ASDisplayNode
@ -520,14 +521,6 @@ open class NavigationController: UINavigationController, ContainableController,
rootModalFrame.updateDismissal(transition: .immediate, progress: 1.0, additionalProgress: 0.0, completion: { [weak rootModalFrame] in
rootModalFrame?.removeFromSupernode()
})
let normalStatusBarStyle: UIStatusBarStyle
switch self.theme.statusBar {
case .black:
normalStatusBarStyle = .default
case .white:
normalStatusBarStyle = .lightContent
}
self.statusBarHost?.setStatusBarStyle(normalStatusBarStyle, animated: false)
}
if let rootContainer = self.rootContainer {
var rootContainerNode: ASDisplayNode
@ -541,16 +534,26 @@ open class NavigationController: UINavigationController, ContainableController,
}
}
if self.validStatusBarStyle != self.theme.statusBar {
self.validStatusBarStyle = self.theme.statusBar
let resolvedStatusBarStyle: NavigationStatusBarStyle
switch statusBarStyle {
case .Ignore, .Hide:
resolvedStatusBarStyle = self.theme.statusBar
case .Black:
resolvedStatusBarStyle = .black
case .White:
resolvedStatusBarStyle = .white
}
if self.validStatusBarStyle != resolvedStatusBarStyle {
self.validStatusBarStyle = resolvedStatusBarStyle
let normalStatusBarStyle: UIStatusBarStyle
switch self.theme.statusBar {
switch resolvedStatusBarStyle {
case .black:
normalStatusBarStyle = .default
case .white:
normalStatusBarStyle = .lightContent
}
self.statusBarHost?.setStatusBarStyle(normalStatusBarStyle, animated: false)
self.statusBarHost?.setStatusBarStyle(normalStatusBarStyle, animated: animateStatusBarStyleTransition)
}
}

View File

@ -19,12 +19,44 @@ NS_ASSUME_NONNULL_BEGIN
@end
@interface TONTransactionId : NSObject
@property (nonatomic, readonly) int64_t lt;
@property (nonatomic, strong, readonly) NSData * _Nonnull transactionHash;
- (instancetype)initWithLt:(int64_t)lt transactionHash:(NSData * _Nonnull)transactionHash;
@end
@interface TONAccountState : NSObject
@property (nonatomic, readonly) int64_t balance;
@property (nonatomic, readonly) int32_t seqno;
@property (nonatomic, strong, readonly) TONTransactionId * _Nullable lastTransactionId;
- (instancetype)initWithBalance:(int64_t)balance seqno:(int32_t)seqno;
- (instancetype)initWithBalance:(int64_t)balance seqno:(int32_t)seqno lastTransactionId:(TONTransactionId * _Nullable)lastTransactionId;
@end
@interface TONTransactionMessage : NSObject
@property (nonatomic, readonly) int64_t value;
@property (nonatomic, strong, readonly) NSString * _Nonnull source;
@property (nonatomic, strong, readonly) NSString * _Nonnull destination;
- (instancetype)initWithValue:(int64_t)value source:(NSString * _Nonnull)source destination:(NSString * _Nonnull)destination;
@end
@interface TONTransaction : NSObject
@property (nonatomic, strong, readonly) NSData * _Nonnull data;
@property (nonatomic, strong, readonly) TONTransactionId * _Nonnull previousTransactionId;
@property (nonatomic, readonly) int64_t fee;
@property (nonatomic, strong, readonly) TONTransactionMessage * _Nullable inMessage;
@property (nonatomic, strong, readonly) NSArray<TONTransactionMessage *> * _Nonnull outMessages;
- (instancetype)initWithData:(NSData * _Nonnull)data previousTransactionId:(TONTransactionId * _Nonnull)previousTransactionId inMessage:(TONTransactionMessage * _Nullable)inMessage outMessages:(NSArray<TONTransactionMessage *> * _Nonnull)outMessages;
@end
@ -35,6 +67,7 @@ NS_ASSUME_NONNULL_BEGIN
- (MTSignal *)createKeyWithLocalPassword:(NSData *)localPassword mnemonicPassword:(NSData *)mnemonicPassword;
- (MTSignal *)getTestWalletAccountAddressWithPublicKey:(NSString *)publicKey;
- (MTSignal *)getTestGiverAccountState;
- (MTSignal *)getTestGiverAddress;
- (MTSignal *)testGiverSendGramsWithAccountState:(TONAccountState *)accountState accountAddress:(NSString *)accountAddress amount:(int64_t)amount;
- (MTSignal *)getAccountStateWithAddress:(NSString *)accountAddress;
- (MTSignal *)sendGramsFromKey:(TONKey *)key localPassword:(NSData *)localPassword fromAddress:(NSString *)fromAddress toAddress:(NSString *)address amount:(int64_t)amount;
@ -42,6 +75,7 @@ NS_ASSUME_NONNULL_BEGIN
- (MTSignal *)importKeyWithLocalPassword:(NSData *)localPassword mnemonicPassword:(NSData *)mnemonicPassword wordList:(NSArray<NSString *> *)wordList;
- (MTSignal *)deleteKeyWithPublicKey:(NSString *)publicKey;
- (MTSignal *)makeWalletInitialized:(TONKey *)key localPassword:(NSData *)localPassword;
- (MTSignal *)getTransactionListWithAddress:(NSString * _Nonnull)address lt:(int64_t)lt hash:(NSData * _Nonnull)hash;
@end

View File

@ -21,6 +21,34 @@ static std::string makeString(NSData * _Nonnull data) {
}
}
static NSData * _Nonnull makeData(std::string &string) {
if (string.size() == 0) {
return [NSData data];
} else {
return [[NSData alloc] initWithBytes:string.data() length:string.size()];
}
}
static NSString * _Nullable readString(std::string &string) {
if (string.size() == 0) {
return @"";
} else {
return [[NSString alloc] initWithBytes:string.data() length:string.size() encoding:NSUTF8StringEncoding];
}
}
static TONTransactionMessage * _Nullable parseTransactionMessage(tonlib_api::object_ptr<tonlib_api::raw_message> &message) {
if (message == nullptr) {
return nil;
}
NSString *source = readString(message->source_);
NSString *destination = readString(message->source_);
if (source == nil || destination == nil) {
return nil;
}
return [[TONTransactionMessage alloc] initWithValue:message->value_ source:source destination:destination];
}
@implementation TONKey
- (instancetype)initWithPublicKey:(NSString *)publicKey secret:(NSData *)secret {
@ -36,11 +64,54 @@ static std::string makeString(NSData * _Nonnull data) {
@implementation TONAccountState
- (instancetype)initWithBalance:(int64_t)balance seqno:(int32_t)seqno {
- (instancetype)initWithBalance:(int64_t)balance seqno:(int32_t)seqno lastTransactionId:(TONTransactionId * _Nullable)lastTransactionId {
self = [super init];
if (self != nil) {
_balance = balance;
_seqno = seqno;
_lastTransactionId = lastTransactionId;
}
return self;
}
@end
@implementation TONTransactionId
- (instancetype)initWithLt:(int64_t)lt transactionHash:(NSData *)transactionHash {
self = [super init];
if (self != nil) {
_lt = lt;
_transactionHash = transactionHash;
}
return self;
}
@end
@implementation TONTransactionMessage
- (instancetype)initWithValue:(int64_t)value source:(NSString * _Nonnull)source destination:(NSString * _Nonnull)destination {
self = [super init];
if (self != nil) {
_value = value;
_source = source;
_destination = destination;
}
return self;
}
@end
@implementation TONTransaction
- (instancetype)initWithData:(NSData * _Nonnull)data previousTransactionId:(TONTransactionId * _Nonnull)previousTransactionId inMessage:(TONTransactionMessage * _Nullable)inMessage outMessages:(NSArray<TONTransactionMessage *> * _Nonnull)outMessages {
self = [super init];
if (self != nil) {
_data = data;
_previousTransactionId = previousTransactionId;
_inMessage = inMessage;
_outMessages = outMessages;
}
return self;
}
@ -272,7 +343,8 @@ typedef enum {
[subscriber putError:[[TONError alloc] initWithText:[[NSString alloc] initWithUTF8String:error->message_.c_str()]]];
} else if (object->get_id() == tonlib_api::testGiver_accountState::ID) {
auto result = tonlib_api::move_object_as<tonlib_api::testGiver_accountState>(object);
[subscriber putNext:[[TONAccountState alloc] initWithBalance:result->balance_ seqno:result->seqno_]];
TONTransactionId *lastTransactionId = [[TONTransactionId alloc] initWithLt:result->last_transaction_id_->lt_ transactionHash:makeData(result->last_transaction_id_->hash_)];
[subscriber putNext:[[TONAccountState alloc] initWithBalance:result->balance_ seqno:result->seqno_ lastTransactionId:lastTransactionId]];
[subscriber putCompletion];
} else {
assert(false);
@ -287,6 +359,32 @@ typedef enum {
}] startOn:[MTQueue mainQueue]] deliverOn:[MTQueue mainQueue]];
}
- (MTSignal *)getTestGiverAddress {
return [[[[MTSignal alloc] initWithGenerator:^id<MTDisposable>(MTSubscriber *subscriber) {
uint64_t requestId = _nextRequestId;
_nextRequestId += 1;
_requestHandlers[@(requestId)] = [[TONRequestHandler alloc] initWithCompletion:^(tonlib_api::object_ptr<tonlib_api::Object> &object) {
if (object->get_id() == tonlib_api::error::ID) {
auto error = tonlib_api::move_object_as<tonlib_api::error>(object);
[subscriber putError:[[TONError alloc] initWithText:[[NSString alloc] initWithUTF8String:error->message_.c_str()]]];
} else if (object->get_id() == tonlib_api::accountAddress::ID) {
auto result = tonlib_api::move_object_as<tonlib_api::accountAddress>(object);
[subscriber putNext:[[NSString alloc] initWithUTF8String:result->account_address_.c_str()]];
[subscriber putCompletion];
} else {
assert(false);
}
}];
auto query = make_object<tonlib_api::testGiver_getAccountAddress>();
_client->send({ requestId, std::move(query) });
return [[MTBlockDisposable alloc] initWithBlock:^{
}];
}] startOn:[MTQueue mainQueue]] deliverOn:[MTQueue mainQueue]];
}
- (MTSignal *)testGiverSendGramsWithAccountState:(TONAccountState *)accountState accountAddress:(NSString *)accountAddress amount:(int64_t)amount {
return [[[[MTSignal alloc] initWithGenerator:^id<MTDisposable>(MTSubscriber *subscriber) {
uint64_t requestId = _nextRequestId;
@ -320,11 +418,17 @@ typedef enum {
[subscriber putError:[[TONError alloc] initWithText:[[NSString alloc] initWithUTF8String:error->message_.c_str()]]];
} else if (object->get_id() == tonlib_api::generic_accountStateUninited::ID) {
auto result = tonlib_api::move_object_as<tonlib_api::generic_accountStateUninited>(object);
[subscriber putNext:[[TONAccountState alloc] initWithBalance:result->account_state_->balance_ seqno:-1]];
[subscriber putNext:[[TONAccountState alloc] initWithBalance:result->account_state_->balance_ seqno:-1 lastTransactionId:nil]];
[subscriber putCompletion];
} else if (object->get_id() == tonlib_api::generic_accountStateTestWallet::ID) {
auto result = tonlib_api::move_object_as<tonlib_api::generic_accountStateTestWallet>(object);
[subscriber putNext:[[TONAccountState alloc] initWithBalance:result->account_state_->balance_ seqno:result->account_state_->seqno_]];
TONTransactionId *lastTransactionId = [[TONTransactionId alloc] initWithLt:result->account_state_->last_transaction_id_->lt_ transactionHash:makeData(result->account_state_->last_transaction_id_->hash_)];
[subscriber putNext:[[TONAccountState alloc] initWithBalance:result->account_state_->balance_ seqno:result->account_state_->seqno_ lastTransactionId:lastTransactionId]];
[subscriber putCompletion];
} else if (object->get_id() == tonlib_api::generic_accountStateTestGiver::ID) {
auto result = tonlib_api::move_object_as<tonlib_api::generic_accountStateTestGiver>(object);
TONTransactionId *lastTransactionId = [[TONTransactionId alloc] initWithLt:result->account_state_->last_transaction_id_->lt_ transactionHash:makeData(result->account_state_->last_transaction_id_->hash_)];
[subscriber putNext:[[TONAccountState alloc] initWithBalance:result->account_state_->balance_ seqno:result->account_state_->seqno_ lastTransactionId:lastTransactionId]];
[subscriber putCompletion];
} else {
assert(false);
@ -533,4 +637,57 @@ typedef enum {
}] startOn:[MTQueue mainQueue]] deliverOn:[MTQueue mainQueue]];
}
- (MTSignal *)getTransactionListWithAddress:(NSString * _Nonnull)address lt:(int64_t)lt hash:(NSData * _Nonnull)hash {
return [[[[MTSignal alloc] initWithGenerator:^id<MTDisposable>(MTSubscriber *subscriber) {
NSData *addressData = [address dataUsingEncoding:NSUTF8StringEncoding];
if (addressData == nil) {
[subscriber putError:[[TONError alloc] initWithText:@"Error encoding UTF8 string in getTransactionListWithAddress"]];
return [[MTBlockDisposable alloc] initWithBlock:^{}];
}
uint64_t requestId = _nextRequestId;
_nextRequestId += 1;
_requestHandlers[@(requestId)] = [[TONRequestHandler alloc] initWithCompletion:^(tonlib_api::object_ptr<tonlib_api::Object> &object) {
if (object->get_id() == tonlib_api::error::ID) {
auto error = tonlib_api::move_object_as<tonlib_api::error>(object);
[subscriber putError:[[TONError alloc] initWithText:[[NSString alloc] initWithUTF8String:error->message_.c_str()]]];
} else if (object->get_id() == tonlib_api::raw_transactions::ID) {
auto result = tonlib_api::move_object_as<tonlib_api::raw_transactions>(object);
NSMutableArray<TONTransaction *> *transactions = [[NSMutableArray alloc] init];
for (auto &it : result->transactions_) {
TONTransactionId *previousTransactionId = [[TONTransactionId alloc] initWithLt:it->previous_transaction_id_->lt_ transactionHash:makeData(it->previous_transaction_id_->hash_)];
TONTransactionMessage *inMessage = parseTransactionMessage(it->in_msg_);
NSMutableArray<TONTransactionMessage *> * outMessages = [[NSMutableArray alloc] init];
for (auto &messageIt : it->out_msgs_) {
TONTransactionMessage *outMessage = parseTransactionMessage(messageIt);
if (outMessage != nil) {
[outMessages addObject:outMessage];
}
}
[transactions addObject:[[TONTransaction alloc] initWithData:makeData(it->data_) previousTransactionId:previousTransactionId inMessage:inMessage outMessages:outMessages]];
}
[subscriber putNext:transactions];
[subscriber putCompletion];
} else {
assert(false);
}
}];
auto query = make_object<tonlib_api::raw_getTransactions>(
make_object<tonlib_api::accountAddress>(
makeString(addressData)
),
make_object<tonlib_api::internal_transactionId>(
lt,
makeString(hash)
)
);
_client->send({ requestId, std::move(query) });
return [[MTBlockDisposable alloc] initWithBlock:^{
}];
}] startOn:[MTQueue mainQueue]] deliverOn:[MTQueue mainQueue]];
}
@end

View File

@ -15,6 +15,7 @@ import TelegramUIPreferences
import ItemListUI
import OverlayStatusController
import AccountContext
import WalletUI
@objc private final class DebugControllerMailComposeDelegate: NSObject, MFMailComposeViewControllerDelegate {
public func mailComposeController(_ controller: MFMailComposeViewController, didFinishWith result: MFMailComposeResult, error: Error?) {
@ -66,7 +67,8 @@ private enum DebugControllerEntry: ItemListNodeEntry {
case resetDatabase(PresentationTheme)
case resetHoles(PresentationTheme)
case resetBiometricsData(PresentationTheme)
case deleteWallets(PresentationTheme)
case openDebugWallet(PresentationTheme)
case getGrams(PresentationTheme)
case optimizeDatabase(PresentationTheme)
case photoPreview(PresentationTheme, Bool)
case knockoutWallpaper(PresentationTheme, Bool)
@ -83,7 +85,7 @@ private enum DebugControllerEntry: ItemListNodeEntry {
return DebugControllerSection.logging.rawValue
case .enableRaiseToSpeak, .keepChatNavigationStack, .skipReadHistory, .crashOnSlowQueries:
return DebugControllerSection.experiments.rawValue
case .clearTips, .reimport, .resetData, .resetDatabase, .resetHoles, .resetBiometricsData, .deleteWallets, .optimizeDatabase, .photoPreview, .knockoutWallpaper, .gradientBubbles:
case .clearTips, .reimport, .resetData, .resetDatabase, .resetHoles, .resetBiometricsData, .openDebugWallet, .getGrams, .optimizeDatabase, .photoPreview, .knockoutWallpaper, .gradientBubbles:
return DebugControllerSection.experiments.rawValue
case .versionInfo:
return DebugControllerSection.info.rawValue
@ -128,18 +130,20 @@ private enum DebugControllerEntry: ItemListNodeEntry {
return 16
case .resetBiometricsData:
return 17
case .deleteWallets:
case .openDebugWallet:
return 18
case .optimizeDatabase:
case .getGrams:
return 19
case .photoPreview:
case .optimizeDatabase:
return 20
case .knockoutWallpaper:
case .photoPreview:
return 21
case .gradientBubbles:
case .knockoutWallpaper:
return 22
case .versionInfo:
case .gradientBubbles:
return 23
case .versionInfo:
return 24
}
}
@ -467,12 +471,44 @@ private enum DebugControllerEntry: ItemListNodeEntry {
return settings.withUpdatedBiometricsDomainState(nil).withUpdatedShareBiometricsDomainState(nil)
}).start()
})
case let .deleteWallets(theme):
case let .openDebugWallet(theme):
return ItemListActionItem(theme: theme, title: "Delete Wallets", kind: .destructive, alignment: .natural, sectionId: self.section, style: .blocks, action: {
guard let context = arguments.context else {
return
}
let _ = debugDeleteWallets(postbox: context.account.postbox).start()
let _ = (availableWallets(postbox: context.account.postbox)
|> deliverOnMainQueue).start(next: { wallets in
if let tonContext = context.tonContext {
if !wallets.wallets.isEmpty {
let _ = (testGiverWalletAddress(tonInstance: tonContext.instance)
|> deliverOnMainQueue).start(next: { address in
arguments.pushController(WalletInfoScreen(context: context, tonContext: tonContext, walletInfo: wallets.wallets[0], address: address))
})
}
}
})
})
case let .getGrams(theme):
return ItemListActionItem(theme: theme, title: "Update Wallet", kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: {
guard let context = arguments.context else {
return
}
let _ = (availableWallets(postbox: context.account.postbox)
|> deliverOnMainQueue).start(next: { wallets in
if let tonContext = context.tonContext {
if !wallets.wallets.isEmpty {
let _ = (walletAddress(publicKey: wallets.wallets[0].publicKey, tonInstance: tonContext.instance)
|> deliverOnMainQueue).start(next: { address in
let _ = (getGramsFromTestGiver(address: address, amount: 1500000000, tonInstance: tonContext.instance)
|> deliverOnMainQueue).start(completed: {
let presentationData = arguments.sharedContext.currentPresentationData.with { $0 }
let controller = OverlayStatusController(theme: presentationData.theme, strings: presentationData.strings, type: .success)
arguments.presentController(controller, nil)
})
})
}
}
})
})
case let .optimizeDatabase(theme):
return ItemListActionItem(theme: theme, title: "Optimize Database", kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: {
@ -556,7 +592,8 @@ private func debugControllerEntries(presentationData: PresentationData, loggingS
entries.append(.resetData(presentationData.theme))
entries.append(.resetDatabase(presentationData.theme))
entries.append(.resetHoles(presentationData.theme))
entries.append(.deleteWallets(presentationData.theme))
entries.append(.openDebugWallet(presentationData.theme))
entries.append(.getGrams(presentationData.theme))
entries.append(.optimizeDatabase(presentationData.theme))
entries.append(.photoPreview(presentationData.theme, experimentalSettings.chatListPhotos))
entries.append(.knockoutWallpaper(presentationData.theme, experimentalSettings.knockoutWallpaper))

View File

@ -6,13 +6,30 @@ import TelegramPresentationData
private let textFont: UIFont = Font.regular(16.0)
public final class SolidRoundedButtonTheme {
public let backgroundColor: UIColor
public let foregroundColor: UIColor
public init(backgroundColor: UIColor, foregroundColor: UIColor) {
self.backgroundColor = backgroundColor
self.foregroundColor = foregroundColor
}
}
public extension SolidRoundedButtonTheme {
convenience init(theme: PresentationTheme) {
self.init(backgroundColor: theme.list.itemCheckColors.fillColor, foregroundColor: theme.list.itemCheckColors.foregroundColor)
}
}
public final class SolidRoundedButtonNode: ASDisplayNode {
private var theme: PresentationTheme
private var theme: SolidRoundedButtonTheme
private let buttonBackgroundNode: ASImageNode
private let buttonGlossNode: SolidRoundedButtonGlossNode
private let buttonNode: HighlightTrackingButtonNode
private let labelNode: ImmediateTextNode
private let iconNode: ASImageNode
private let buttonHeight: CGFloat
private let buttonCornerRadius: CGFloat
@ -28,7 +45,7 @@ public final class SolidRoundedButtonNode: ASDisplayNode {
}
}
public init(title: String? = nil, theme: PresentationTheme, height: CGFloat = 48.0, cornerRadius: CGFloat = 24.0, gloss: Bool = false) {
public init(title: String? = nil, icon: UIImage? = nil, theme: SolidRoundedButtonTheme, height: CGFloat = 48.0, cornerRadius: CGFloat = 24.0, gloss: Bool = false) {
self.theme = theme
self.buttonHeight = height
self.buttonCornerRadius = cornerRadius
@ -38,15 +55,20 @@ public final class SolidRoundedButtonNode: ASDisplayNode {
self.buttonBackgroundNode.isLayerBacked = true
self.buttonBackgroundNode.displayWithoutProcessing = true
self.buttonBackgroundNode.displaysAsynchronously = false
self.buttonBackgroundNode.image = generateStretchableFilledCircleImage(radius: cornerRadius, color: theme.list.itemCheckColors.fillColor)
self.buttonBackgroundNode.image = generateStretchableFilledCircleImage(radius: cornerRadius, color: theme.backgroundColor)
self.buttonGlossNode = SolidRoundedButtonGlossNode(color: theme.list.itemCheckColors.foregroundColor, cornerRadius: cornerRadius)
self.buttonGlossNode = SolidRoundedButtonGlossNode(color: theme.foregroundColor, cornerRadius: cornerRadius)
self.buttonNode = HighlightTrackingButtonNode()
self.labelNode = ImmediateTextNode()
self.labelNode.isUserInteractionEnabled = false
self.iconNode = ASImageNode()
self.iconNode.displayWithoutProcessing = true
self.iconNode.displaysAsynchronously = false
self.iconNode.image = icon
super.init()
self.addSubnode(self.buttonBackgroundNode)
@ -55,6 +77,7 @@ public final class SolidRoundedButtonNode: ASDisplayNode {
}
self.addSubnode(self.buttonNode)
self.addSubnode(self.labelNode)
self.addSubnode(self.iconNode)
self.buttonNode.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside)
self.buttonNode.highligthedChanged = { [weak self] highlighted in
@ -70,15 +93,15 @@ public final class SolidRoundedButtonNode: ASDisplayNode {
}
}
public func updateTheme(_ theme: PresentationTheme) {
public func updateTheme(_ theme: SolidRoundedButtonTheme) {
guard theme !== self.theme else {
return
}
self.theme = theme
self.buttonBackgroundNode.image = generateStretchableFilledCircleImage(radius: self.buttonCornerRadius, color: theme.list.itemCheckColors.fillColor)
self.buttonGlossNode.color = theme.list.itemCheckColors.foregroundColor
self.labelNode.attributedText = NSAttributedString(string: self.title ?? "", font: Font.medium(17.0), textColor: theme.list.itemCheckColors.foregroundColor)
self.buttonBackgroundNode.image = generateStretchableFilledCircleImage(radius: self.buttonCornerRadius, color: theme.backgroundColor)
self.buttonGlossNode.color = theme.foregroundColor
self.labelNode.attributedText = NSAttributedString(string: self.title ?? "", font: Font.semibold(17.0), textColor: theme.foregroundColor)
}
public func updateLayout(width: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat {
@ -91,11 +114,25 @@ public final class SolidRoundedButtonNode: ASDisplayNode {
transition.updateFrame(node: self.buttonNode, frame: buttonFrame)
if self.title != self.labelNode.attributedText?.string {
self.labelNode.attributedText = NSAttributedString(string: self.title ?? "", font: Font.medium(17.0), textColor: self.theme.list.itemCheckColors.foregroundColor)
self.labelNode.attributedText = NSAttributedString(string: self.title ?? "", font: Font.semibold(17.0), textColor: self.theme.foregroundColor)
}
let iconSize = self.iconNode.image?.size ?? CGSize()
let labelSize = self.labelNode.updateLayout(buttonSize)
let labelFrame = CGRect(origin: CGPoint(x: buttonFrame.minX + floor((buttonFrame.width - labelSize.width) / 2.0), y: buttonFrame.minY + floor((buttonFrame.height - labelSize.height) / 2.0)), size: labelSize)
let iconSpacing: CGFloat = 8.0
var contentWidth: CGFloat = labelSize.width
if !iconSize.width.isZero {
contentWidth += iconSize.width + iconSpacing
}
var nextContentOrigin = floor((buttonFrame.width - contentWidth) / 2.0)
transition.updateFrame(node: self.iconNode, frame: CGRect(origin: CGPoint(x: buttonFrame.minX + nextContentOrigin, y: floor((buttonFrame.height - iconSize.height) / 2.0)), size: iconSize))
if !iconSize.width.isZero {
nextContentOrigin += iconSize.width + iconSpacing
}
let labelFrame = CGRect(origin: CGPoint(x: buttonFrame.minX + nextContentOrigin, y: buttonFrame.minY + floor((buttonFrame.height - labelSize.height) / 2.0)), size: labelSize)
transition.updateFrame(node: self.labelNode, frame: labelFrame)
return buttonSize.height

View File

@ -16,7 +16,7 @@ public enum StickerPackPreviewControllerMode {
case settings
}
public final class StickerPackPreviewController: ViewController {
public final class StickerPackPreviewController: ViewController, StandalonePresentableController {
private var controllerNode: StickerPackPreviewControllerNode {
return self.displayNode as! StickerPackPreviewControllerNode
}

View File

@ -158,6 +158,31 @@ public final class TonInstance {
}
}
fileprivate func testGiverWalletAddress() -> Signal<String, NoError> {
return Signal { subscriber in
let disposable = MetaDisposable()
self.impl.with { impl in
impl.withInstance { ton in
let cancel = ton.getTestGiverAddress().start(next: { address in
guard let address = address as? String else {
return
}
subscriber.putNext(address)
subscriber.putCompletion()
}, error: { _ in
}, completed: {
})
disposable.set(ActionDisposable {
cancel?.dispose()
})
}
}
return disposable
}
}
fileprivate func walletBalance(publicKey: WalletPublicKey) -> Signal<WalletBalance, NoError> {
return Signal { subscriber in
let disposable = MetaDisposable()
@ -193,6 +218,92 @@ public final class TonInstance {
}
}
fileprivate func walletLastTransactionId(address: String) -> Signal<WalletTransactionId?, NoError> {
return Signal { subscriber in
let disposable = MetaDisposable()
self.impl.with { impl in
impl.withInstance { ton in
let cancel = ton.getAccountState(withAddress: address).start(next: { state in
guard let state = state as? TONAccountState else {
subscriber.putNext(nil)
return
}
subscriber.putNext(state.lastTransactionId.flatMap(WalletTransactionId.init(tonTransactionId:)))
}, error: { _ in
}, completed: {
subscriber.putCompletion()
})
disposable.set(ActionDisposable {
cancel?.dispose()
})
}
}
return disposable
}
}
fileprivate func getWalletTransactions(address: String, previousId: WalletTransactionId) -> Signal<[WalletTransaction], GetWalletTransactionsError> {
return Signal { subscriber in
let disposable = MetaDisposable()
self.impl.with { impl in
impl.withInstance { ton in
let cancel = ton.getTransactionList(withAddress: address, lt: previousId.lt, hash: previousId.transactionHash).start(next: { transactions in
guard let transactions = transactions as? [TONTransaction] else {
subscriber.putError(.generic)
return
}
subscriber.putNext(transactions.map(WalletTransaction.init(tonTransaction:)))
}, error: { _ in
}, completed: {
subscriber.putCompletion()
})
disposable.set(ActionDisposable {
cancel?.dispose()
})
}
}
return disposable
}
}
fileprivate func getGramsFromTestGiver(address: String, amount: Int64) -> Signal<Void, GetGramsFromTestGiverError> {
return Signal { subscriber in
let disposable = MetaDisposable()
self.impl.with { impl in
impl.withInstance { ton in
let cancel = ton.getTestGiverAccountState().start(next: { state in
guard let state = state as? TONAccountState else {
subscriber.putError(.generic)
return
}
let cancel = ton.testGiverSendGrams(with: state, accountAddress: address, amount: amount).start(next: { _ in
}, error: { _ in
subscriber.putError(.generic)
}, completed: {
subscriber.putCompletion()
})
disposable.set(ActionDisposable {
cancel?.dispose()
})
}, error: { _ in
subscriber.putError(.generic)
}, completed: {
})
disposable.set(ActionDisposable {
cancel?.dispose()
})
}
}
return disposable
}
}
fileprivate func walletRestoreWords(walletInfo: WalletInfo, keychain: TonKeychain, serverSalt: Data) -> Signal<[String], WalletRestoreWordsError> {
return keychain.decrypt(walletInfo.encryptedSecret.rawValue)
|> introduceError(WalletRestoreWordsError.self)
@ -385,19 +496,145 @@ public func walletAddress(publicKey: WalletPublicKey, tonInstance: TonInstance)
return tonInstance.walletAddress(publicKey: publicKey)
}
public func testGiverWalletAddress(tonInstance: TonInstance) -> Signal<String, NoError> {
return tonInstance.testGiverWalletAddress()
}
public func currentWalletBalance(publicKey: WalletPublicKey, tonInstance: TonInstance) -> Signal<WalletBalance, NoError> {
return tonInstance.walletBalance(publicKey: publicKey)
}
public enum GetGramsFromTestGiverError {
case generic
}
public func getGramsFromTestGiver(address: String, amount: Int64, tonInstance: TonInstance) -> Signal<Void, GetGramsFromTestGiverError> {
return tonInstance.getGramsFromTestGiver(address: address, amount: amount)
}
public struct WalletTransactionId: Hashable {
public var lt: Int64
public var transactionHash: Data
}
private extension WalletTransactionId {
init(tonTransactionId: TONTransactionId) {
self.lt = tonTransactionId.lt
self.transactionHash = tonTransactionId.transactionHash
}
}
public final class WalletTransactionMessage: Equatable {
public let value: Int64
public let source: String
public let destination: String
init(value: Int64, source: String, destination: String) {
self.value = value
self.source = source
self.destination = destination
}
public static func ==(lhs: WalletTransactionMessage, rhs: WalletTransactionMessage) -> Bool {
if lhs.value != rhs.value {
return false
}
if lhs.source != rhs.source {
return false
}
if lhs.destination != rhs.destination {
return false;
}
return true
}
}
private extension WalletTransactionMessage {
convenience init(tonTransactionMessage: TONTransactionMessage) {
self.init(value: tonTransactionMessage.value, source: tonTransactionMessage.source, destination: tonTransactionMessage.destination)
}
}
public final class WalletTransaction: Equatable {
public let data: Data
public let previousTransactionId: WalletTransactionId
public let fee: Int64
public let inMessage: WalletTransactionMessage?
public let outMessages: [WalletTransactionMessage]
public var transferredValue: Int64 {
var value: Int64 = 0
if let inMessage = self.inMessage {
value += inMessage.value
}
for message in self.outMessages {
value -= message.value
}
value -= self.fee
return value
}
init(data: Data, previousTransactionId: WalletTransactionId, fee: Int64, inMessage: WalletTransactionMessage?, outMessages: [WalletTransactionMessage]) {
self.data = data
self.previousTransactionId = previousTransactionId
self.fee = fee
self.inMessage = inMessage
self.outMessages = outMessages
}
public static func ==(lhs: WalletTransaction, rhs: WalletTransaction) -> Bool {
if lhs.data != rhs.data {
return false
}
if lhs.previousTransactionId != rhs.previousTransactionId {
return false
}
if lhs.fee != rhs.fee {
return false
}
if lhs.inMessage != rhs.inMessage {
return false
}
if lhs.outMessages != rhs.outMessages {
return false
}
return true
}
}
private extension WalletTransaction {
convenience init(tonTransaction: TONTransaction) {
self.init(data: tonTransaction.data, previousTransactionId: WalletTransactionId(tonTransactionId: tonTransaction.previousTransactionId), fee: tonTransaction.fee, inMessage: tonTransaction.inMessage.flatMap(WalletTransactionMessage.init(tonTransactionMessage:)), outMessages: tonTransaction.outMessages.map(WalletTransactionMessage.init(tonTransactionMessage:)))
}
}
public enum GetWalletTransactionsError {
case generic
}
public func getWalletTransactions(address: String, previousId: WalletTransactionId?, tonInstance: TonInstance) -> Signal<[WalletTransaction], GetWalletTransactionsError> {
let previousIdValue: Signal<WalletTransactionId?, GetWalletTransactionsError>
if let previousId = previousId {
previousIdValue = .single(previousId)
} else {
previousIdValue = tonInstance.walletLastTransactionId(address: address)
|> introduceError(GetWalletTransactionsError.self)
}
return previousIdValue
|> mapToSignal { previousId in
if let previousId = previousId {
return tonInstance.getWalletTransactions(address: address, previousId: previousId)
} else {
return .single([])
}
}
}
public enum GetServerWalletSaltError {
case generic
}
private func getServerWalletSalt(network: Network) -> Signal<Data, GetServerWalletSaltError> {
#if DEBUG
return .single(Data())
#endif
return network.request(Api.functions.wallet.getKeySecretSalt(revoke: .boolFalse))
|> mapError { _ -> GetServerWalletSaltError in
return .generic

View File

@ -83,7 +83,7 @@ public final class PermissionContentNode: ASDisplayNode {
self.textNode.maximumNumberOfLines = 0
self.textNode.displaysAsynchronously = false
self.actionButton = SolidRoundedButtonNode(theme: theme, height: 48.0, cornerRadius: 9.0, gloss: true)
self.actionButton = SolidRoundedButtonNode(theme: SolidRoundedButtonTheme(theme: theme), height: 48.0, cornerRadius: 9.0, gloss: true)
self.footerNode = ImmediateTextNode()
self.footerNode.textAlignment = .center

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "gem.pdf"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

View File

@ -0,0 +1,9 @@
{
"info" : {
"version" : 1,
"author" : "xcode"
},
"properties" : {
"provides-namespace" : true
}
}

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "duck.pdf"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "NavigationSettingsIcon.pdf"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "Group2.pdf"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "Group.pdf"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "SmallGem.pdf"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

View File

@ -72,7 +72,7 @@ class ChatScheduleTimeControllerNode: ViewControllerTracingNode, UIScrollViewDel
self.cancelButton = HighlightableButtonNode()
self.cancelButton.setTitle(self.presentationData.strings.Common_Cancel, with: Font.regular(17.0), with: self.presentationData.theme.actionSheet.controlAccentColor, for: .normal)
self.doneButton = SolidRoundedButtonNode(theme: self.presentationData.theme, height: 52.0, cornerRadius: 11.0, gloss: false)
self.doneButton = SolidRoundedButtonNode(theme: SolidRoundedButtonTheme(theme: self.presentationData.theme), height: 52.0, cornerRadius: 11.0, gloss: false)
self.dateFormatter = DateFormatter()
self.dateFormatter.timeStyle = .none
@ -152,7 +152,7 @@ class ChatScheduleTimeControllerNode: ViewControllerTracingNode, UIScrollViewDel
}
self.cancelButton.setTitle(self.presentationData.strings.Common_Cancel, with: Font.regular(17.0), with: self.presentationData.theme.actionSheet.controlAccentColor, for: .normal)
self.doneButton.updateTheme(self.presentationData.theme)
self.doneButton.updateTheme(SolidRoundedButtonTheme(theme: self.presentationData.theme))
}
private func updateMinimumDate(currentTime: Int32? = nil) {

View File

@ -1,13 +1,11 @@
{
"@type": "config.global",
"liteservers": [
{
"@type": "liteserver.config.global",
"ip": -1304504083,
"ip": 1137658550,
"port": 4924,
"id": {
"@type": "pub.ed25519",
"key": "keoMMlpiIJPaY4SeXQlie8cOL6XNo81zJqrqbyVrPAQ="
"key": "peJTw/arlRfssgTuf9BMypJzqOi7SXEqSPSWiEw2U1M="
}
}
],
@ -17,8 +15,8 @@
"workchain": -1,
"shard": -9223372036854775808,
"seqno": 0,
"root_hash": "XV54jYRsGWJ301YvzibvM4ygmDrxxn4LwcyMa/0NOQ0=",
"file_hash": "9nhKiv4g5fmliP9HayeebodXxjLw+J5sYDWblE75p9U="
"root_hash": "VCSXxDHhTALFxReyTZRd8E4Ya3ySOmpOWAS4rBX9XBY=",
"file_hash": "eh9yveSz1qMdJ7mOsO+I+H77jkLr9NpAuEkoJuseXBo="
}
}
}

View File

@ -128,6 +128,7 @@ public func updateInfoController(context: AccountContext, appUpdateInfo: AppUpda
}
let controller = ItemListController(sharedContext: context.sharedContext, state: signal)
controller.navigationPresentation = .modal
linkActionImpl = { [weak controller, weak context] action, itemLink in
if let strongController = controller, let context = context {
context.sharedContext.handleTextLinkAction(context: context, peerId: nil, navigateDisposable: navigateDisposable, controller: strongController, action: action, itemLink: itemLink)

View File

@ -18,6 +18,8 @@ static_library(
"//submodules/AccountContext:AccountContext",
"//submodules/UndoUI:UndoUI",
"//submodules/AlertUI:AlertUI",
"//submodules/MergeLists:MergeLists",
"//submodules/ItemListUI:ItemListUI",
],
frameworks = [
"$SDKROOT/System/Library/Frameworks/Foundation.framework",

View File

@ -0,0 +1,91 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import TelegramPresentationData
import TelegramCore
import AnimationUI
final class WalletInfoEmptyNode: ASDisplayNode {
private var presentationData: PresentationData
private let iconNode: ASImageNode
private let animationNode: AnimatedStickerNode
private let titleNode: ImmediateTextNode
private let textNode: ImmediateTextNode
private let addressNode: ImmediateTextNode
init(presentationData: PresentationData, address: String) {
self.presentationData = presentationData
self.iconNode = ASImageNode()
self.iconNode.displayWithoutProcessing = true
self.iconNode.displaysAsynchronously = false
self.animationNode = AnimatedStickerNode()
let title = "Wallet Created"
let text = "Your wallet address"
self.iconNode.image = UIImage(bundleImageName: "Wallet/DuckIcon")
self.titleNode = ImmediateTextNode()
self.titleNode.displaysAsynchronously = false
self.titleNode.attributedText = NSAttributedString(string: title, font: Font.bold(32.0), textColor: self.presentationData.theme.list.itemPrimaryTextColor)
self.titleNode.maximumNumberOfLines = 0
self.titleNode.textAlignment = .center
self.textNode = ImmediateTextNode()
self.textNode.displaysAsynchronously = false
self.textNode.attributedText = NSAttributedString(string: text, font: Font.regular(16.0), textColor: self.presentationData.theme.list.itemPrimaryTextColor)
self.textNode.maximumNumberOfLines = 0
self.textNode.lineSpacing = 0.1
self.textNode.textAlignment = .center
self.addressNode = ImmediateTextNode()
self.addressNode.displaysAsynchronously = false
self.addressNode.attributedText = NSAttributedString(string: address, font: Font.monospace(16.0), textColor: self.presentationData.theme.list.itemPrimaryTextColor)
self.addressNode.maximumNumberOfLines = 0
self.addressNode.lineSpacing = 0.1
self.addressNode.textAlignment = .center
super.init()
self.addSubnode(self.iconNode)
self.addSubnode(self.animationNode)
self.addSubnode(self.titleNode)
self.addSubnode(self.textNode)
self.addSubnode(self.addressNode)
}
func updateLayout(width: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat {
let sideInset: CGFloat = 32.0
let buttonSideInset: CGFloat = 48.0
let iconSpacing: CGFloat = 5.0
let titleSpacing: CGFloat = 19.0
let termsSpacing: CGFloat = 11.0
let buttonHeight: CGFloat = 50.0
let iconSize: CGSize
var iconOffset = CGPoint()
iconSize = self.iconNode.image?.size ?? CGSize(width: 140.0, height: 140.0)
let titleSize = self.titleNode.updateLayout(CGSize(width: width - sideInset * 2.0, height: .greatestFiniteMagnitude))
let textSize = self.textNode.updateLayout(CGSize(width: width - sideInset * 2.0, height: .greatestFiniteMagnitude))
let addressSize = self.addressNode.updateLayout(CGSize(width: width - sideInset * 2.0, height: .greatestFiniteMagnitude))
let contentVerticalOrigin: CGFloat = 0.0
let iconFrame = CGRect(origin: CGPoint(x: floor((width - iconSize.width) / 2.0), y: contentVerticalOrigin), size: iconSize).offsetBy(dx: iconOffset.x, dy: iconOffset.y)
transition.updateFrameAdditive(node: self.iconNode, frame: iconFrame)
self.animationNode.updateLayout(size: iconFrame.size)
transition.updateFrameAdditive(node: self.animationNode, frame: iconFrame)
let titleFrame = CGRect(origin: CGPoint(x: floor((width - titleSize.width) / 2.0), y: iconFrame.maxY + iconSpacing), size: titleSize)
transition.updateFrameAdditive(node: self.titleNode, frame: titleFrame)
let textFrame = CGRect(origin: CGPoint(x: floor((width - textSize.width) / 2.0), y: titleFrame.maxY + titleSpacing), size: textSize)
transition.updateFrameAdditive(node: self.textNode, frame: textFrame)
let addressFrame = CGRect(origin: CGPoint(x: floor((width - addressSize.width) / 2.0), y: textFrame.maxY + titleSpacing), size: addressSize)
transition.updateFrameAdditive(node: self.addressNode, frame: addressFrame)
return addressFrame.maxY
}
}

View File

@ -10,6 +10,7 @@ import TelegramCore
import SolidRoundedButtonNode
import AnimationUI
import SwiftSignalKit
import MergeLists
public final class WalletInfoScreen: ViewController {
private let context: AccountContext
@ -28,15 +29,21 @@ public final class WalletInfoScreen: ViewController {
self.presentationData = context.sharedContext.currentPresentationData.with { $0 }
let defaultNavigationPresentationData = NavigationBarPresentationData(presentationTheme: self.presentationData.theme, presentationStrings: self.presentationData.strings)
let navigationBarTheme = NavigationBarTheme(buttonColor: defaultNavigationPresentationData.theme.buttonColor, disabledButtonColor: defaultNavigationPresentationData.theme.disabledButtonColor, primaryTextColor: defaultNavigationPresentationData.theme.primaryTextColor, backgroundColor: .clear, separatorColor: .clear, badgeBackgroundColor: defaultNavigationPresentationData.theme.badgeBackgroundColor, badgeStrokeColor: defaultNavigationPresentationData.theme.badgeStrokeColor, badgeTextColor: defaultNavigationPresentationData.theme.badgeTextColor)
let navigationBarTheme = NavigationBarTheme(buttonColor: .white, disabledButtonColor: .white, primaryTextColor: .white, backgroundColor: .clear, separatorColor: .clear, badgeBackgroundColor: defaultNavigationPresentationData.theme.badgeBackgroundColor, badgeStrokeColor: defaultNavigationPresentationData.theme.badgeStrokeColor, badgeTextColor: defaultNavigationPresentationData.theme.badgeTextColor)
super.init(navigationBarPresentationData: NavigationBarPresentationData(theme: navigationBarTheme, strings: defaultNavigationPresentationData.strings))
self.statusBar.statusBarStyle = .White
self.navigationPresentation = .modalInLargeLayout
self.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait)
self.navigationBar?.intrinsicCanTransitionInline = false
self.navigationItem.backBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Back, style: .plain, target: nil, action: nil)
self.navigationItem.rightBarButtonItem = UIBarButtonItem(image: generateTintedImage(image: UIImage(bundleImageName: "Wallet/NavigationSettingsIcon"), color: .white), style: .plain, target: self, action: #selector(self.settingsPressed))
self.scrollToTop = { [weak self] in
(self?.displayNode as? WalletInfoScreenNode)?.scrollToTop()
}
}
required init(coder aDecoder: NSCoder) {
@ -47,22 +54,20 @@ public final class WalletInfoScreen: ViewController {
self.dismiss()
}
@objc private func settingsPressed() {
self.push(walletSettingsController(context: self.context, tonContext: self.tonContext, walletInfo: self.walletInfo))
}
override public func loadDisplayNode() {
self.displayNode = WalletInfoScreenNode(account: self.context.account, tonContext: self.tonContext, presentationData: self.presentationData, walletInfo: self.walletInfo, address: self.address, exportAction: { [weak self] in
self.displayNode = WalletInfoScreenNode(account: self.context.account, tonContext: self.tonContext, presentationData: self.presentationData, walletInfo: self.walletInfo, address: self.address, sendAction: {
}, receiveAction: {
}, openTransaction: { [weak self] transaction in
guard let strongSelf = self else {
return
}
let _ = (walletRestoreWords(network: strongSelf.context.account.network, walletInfo: strongSelf.walletInfo, tonInstance: strongSelf.tonContext.instance, keychain: strongSelf.tonContext.keychain)
|> deliverOnMainQueue).start(next: { wordList in
guard let strongSelf = self else {
return
}
strongSelf.push(WalletWordDisplayScreen(context: strongSelf.context, tonContext: strongSelf.tonContext, walletInfo: strongSelf.walletInfo, wordList: wordList))
}, error: { _ in
guard let strongSelf = self else {
return
}
})
strongSelf.push(walletTransactionInfoController(context: strongSelf.context, walletTransaction: transaction))
})
self.displayNodeDidLoad()
@ -75,102 +80,516 @@ public final class WalletInfoScreen: ViewController {
}
}
private final class WalletInfoScreenNode: ViewControllerTracingNode {
private var presentationData: PresentationData
private let walletInfo: WalletInfo
private let exportAction: () -> Void
private final class WalletInfoBalanceNode: ASDisplayNode {
private let balanceTextNode: ImmediateTextNode
private let balanceIconNode: ASImageNode
private let titleNode: ImmediateTextNode
private let textNode: ImmediateTextNode
private let buttonNode: SolidRoundedButtonNode
var inProgress: Bool = false {
var balance: String = " " {
didSet {
self.buttonNode.isUserInteractionEnabled = !self.inProgress
self.buttonNode.alpha = self.inProgress ? 0.6 : 1.0
self.balanceTextNode.attributedText = NSAttributedString(string: self.balance, font: Font.bold(39.0), textColor: .white)
}
}
init(theme: PresentationTheme) {
self.balanceTextNode = ImmediateTextNode()
self.balanceTextNode.displaysAsynchronously = false
self.balanceTextNode.attributedText = NSAttributedString(string: " ", font: Font.bold(39.0), textColor: .white)
self.balanceTextNode.layer.minificationFilter = .linear
self.balanceIconNode = ASImageNode()
self.balanceIconNode.displaysAsynchronously = false
self.balanceIconNode.displayWithoutProcessing = true
self.balanceIconNode.image = UIImage(bundleImageName: "Wallet/BalanceGem")?.precomposed()
super.init()
self.addSubnode(self.balanceTextNode)
self.addSubnode(self.balanceIconNode)
}
func update(width: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat {
let sideInset: CGFloat = 16.0
let balanceIconSpacing: CGFloat = 8.0
let balanceTextSize = self.balanceTextNode.updateLayout(CGSize(width: width - sideInset * 2.0, height: 200.0))
let balanceIconSize = self.balanceIconNode.image?.size ?? CGSize(width: 38.0, height: 34.0)
let balanceOrigin = CGPoint(x: floor((width - balanceTextSize.width - balanceIconSpacing - balanceIconSize.width / 2.0) / 2.0), y: 0.0)
let balanceTextFrame = CGRect(origin: balanceOrigin, size: balanceTextSize)
let balanceIconFrame = CGRect(origin: CGPoint(x: balanceTextFrame.maxX + balanceIconSpacing, y: balanceTextFrame.minY + floor((balanceTextFrame.height - balanceIconSize.height) / 2.0)), size: balanceIconSize)
transition.updateFrameAdditive(node: self.balanceTextNode, frame: balanceTextFrame)
transition.updateFrameAdditive(node: self.balanceIconNode, frame: balanceIconFrame)
return balanceTextSize.height
}
}
private final class WalletInfoHeaderNode: ASDisplayNode {
var balance: Int64?
let balanceNode: WalletInfoBalanceNode
private let balanceSubtitleNode: ImmediateTextNode
private let receiveButtonNode: SolidRoundedButtonNode
private let sendButtonNode: SolidRoundedButtonNode
private let headerBackgroundNode: ASImageNode
init(theme: PresentationTheme, sendAction: @escaping () -> Void, receiveAction: @escaping () -> Void) {
self.balanceNode = WalletInfoBalanceNode(theme: theme)
self.balanceSubtitleNode = ImmediateTextNode()
self.balanceSubtitleNode.displaysAsynchronously = false
self.balanceSubtitleNode.attributedText = NSAttributedString(string: "your balance", font: Font.regular(13), textColor: UIColor(white: 1.0, alpha: 0.6))
self.headerBackgroundNode = ASImageNode()
self.headerBackgroundNode.displaysAsynchronously = false
self.headerBackgroundNode.displayWithoutProcessing = true
self.headerBackgroundNode.image = generateImage(CGSize(width: 20.0, height: 20.0), rotatedContext: { size, context in
context.setFillColor(UIColor.black.cgColor)
context.fill(CGRect(origin: CGPoint(), size: size))
context.setBlendMode(.copy)
context.setFillColor(UIColor.clear.cgColor)
context.fillEllipse(in: CGRect(origin: CGPoint(x: 0.0, y: size.height / 2.0), size: size))
})?.stretchableImage(withLeftCapWidth: 10, topCapHeight: 1)
self.receiveButtonNode = SolidRoundedButtonNode(title: "Receive", icon: UIImage(bundleImageName: "Wallet/ReceiveButtonIcon"), theme: SolidRoundedButtonTheme(backgroundColor: .white, foregroundColor: .black), height: 50.0, cornerRadius: 10.0, gloss: false)
self.sendButtonNode = SolidRoundedButtonNode(title: "Send", icon: UIImage(bundleImageName: "Wallet/SendButtonIcon"), theme: SolidRoundedButtonTheme(backgroundColor: .white, foregroundColor: .black), height: 50.0, cornerRadius: 10.0, gloss: false)
super.init()
self.addSubnode(self.headerBackgroundNode)
self.addSubnode(self.receiveButtonNode)
self.addSubnode(self.sendButtonNode)
self.addSubnode(self.balanceNode)
self.addSubnode(self.balanceSubtitleNode)
self.receiveButtonNode.pressed = {
receiveAction()
}
self.sendButtonNode.pressed = {
sendAction()
}
}
func update(size: CGSize, navigationHeight: CGFloat, offset: CGFloat, transition: ContainedViewLayoutTransition) {
let sideInset: CGFloat = 16.0
let buttonSideInset: CGFloat = 48.0
let titleSpacing: CGFloat = 10.0
let termsSpacing: CGFloat = 10.0
let buttonHeight: CGFloat = 50.0
let balanceSubtitleSpacing: CGFloat = 0.0
let minOffset = navigationHeight
let maxOffset = size.height
let minHeaderOffset = minOffset
let maxHeaderOffset = (minOffset + maxOffset) / 2.0
let effectiveOffset = max(offset, navigationHeight)
let minButtonsOffset = maxOffset - buttonHeight - sideInset
let maxButtonsOffset = maxOffset
let buttonTransition: CGFloat = max(0.0, min(1.0, (effectiveOffset - minButtonsOffset) / (maxButtonsOffset - minButtonsOffset)))
let buttonAlpha = buttonTransition * 1.0
let balanceSubtitleSize = self.balanceSubtitleNode.updateLayout(CGSize(width: size.width - sideInset * 2.0, height: 200.0))
let balanceHeight = self.balanceNode.update(width: size.width, transition: transition)
let balanceSize = CGSize(width: size.width, height: balanceHeight)
let minHeaderScale: CGFloat = 0.435
let minHeaderHeight: CGFloat = balanceSize.height + balanceSubtitleSize.height + balanceSubtitleSpacing
let minHeaderY = navigationHeight - 44.0 + floor((44.0 - minHeaderHeight) / 2.0)
let maxHeaderY = floor((size.height - balanceSize.height) / 2.0)
let headerScaleTransition: CGFloat = max(0.0, min(1.0, (effectiveOffset - minHeaderOffset) / (maxHeaderOffset - minHeaderOffset)))
let headerPositionTransition: CGFloat = max(0.0, (effectiveOffset - minHeaderOffset) / (maxOffset - minHeaderOffset))
let headerY = headerPositionTransition * maxHeaderY + (1.0 - headerPositionTransition) * minHeaderY
let headerScale = headerScaleTransition * 1.0 + (1.0 - headerScaleTransition) * minHeaderScale
let balanceFrame = CGRect(origin: CGPoint(x: 0.0, y: headerY), size: balanceSize)
transition.updateFrame(node: self.balanceNode, frame: balanceFrame)
transition.updateSublayerTransformScale(node: self.balanceNode, scale: headerScale)
transition.updateFrameAdditive(node: self.balanceSubtitleNode, frame: CGRect(origin: CGPoint(x: floor((size.width - balanceSubtitleSize.width) / 2.0), y: balanceFrame.midY + (balanceFrame.height / 2.0 * headerScale) + balanceSubtitleSpacing), size: balanceSubtitleSize))
let headerHeight: CGFloat = 1000.0
transition.updateFrame(node: self.headerBackgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: effectiveOffset + 10.0 - headerHeight), size: CGSize(width: size.width, height: headerHeight)))
let leftButtonFrame = CGRect(origin: CGPoint(x: sideInset, y: effectiveOffset - sideInset - buttonHeight), size: CGSize(width: floor((size.width - sideInset * 3.0) / 2.0), height: buttonHeight))
let sendButtonFrame = CGRect(origin: CGPoint(x: leftButtonFrame.maxX + sideInset, y: leftButtonFrame.minY), size: CGSize(width: size.width - leftButtonFrame.maxX - sideInset * 2.0, height: buttonHeight))
let fullButtonFrame = CGRect(origin: CGPoint(x: sideInset, y: effectiveOffset - sideInset - buttonHeight), size: CGSize(width: size.width - sideInset * 2.0, height: buttonHeight))
var receiveButtonFrame: CGRect
if let balance = self.balance, balance > 0 {
receiveButtonFrame = leftButtonFrame
self.receiveButtonNode.isHidden = false
self.sendButtonNode.isHidden = false
} else {
receiveButtonFrame = fullButtonFrame
if self.balance == nil {
self.receiveButtonNode.isHidden = true
self.sendButtonNode.isHidden = true
} else {
self.receiveButtonNode.isHidden = false
self.sendButtonNode.isHidden = true
}
}
if self.balance == nil {
self.balanceNode.isHidden = true
self.balanceSubtitleNode.isHidden = true
} else {
self.balanceNode.isHidden = false
self.balanceSubtitleNode.isHidden = false
}
transition.updateFrame(node: self.receiveButtonNode, frame: receiveButtonFrame)
transition.updateAlpha(node: self.receiveButtonNode, alpha: buttonAlpha)
self.receiveButtonNode.updateLayout(width: receiveButtonFrame.width, transition: transition)
transition.updateFrame(node: self.sendButtonNode, frame: sendButtonFrame)
transition.updateAlpha(node: self.sendButtonNode, alpha: buttonAlpha)
self.sendButtonNode.updateLayout(width: sendButtonFrame.width, transition: transition)
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if let result = self.sendButtonNode.hitTest(self.view.convert(point, to: self.sendButtonNode.view), with: event) {
return result
}
if let result = self.receiveButtonNode.hitTest(self.view.convert(point, to: self.receiveButtonNode.view), with: event) {
return result
}
return nil
}
func animateIn() {
self.sendButtonNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
self.receiveButtonNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
self.balanceNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
self.balanceSubtitleNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
}
}
private struct WalletInfoListTransaction {
let deletions: [ListViewDeleteItem]
let insertions: [ListViewInsertItem]
let updates: [ListViewUpdateItem]
}
private struct WalletInfoListEntry: Equatable, Comparable, Identifiable {
let index: Int
let item: WalletTransaction
var stableId: WalletTransactionId {
return self.item.previousTransactionId
}
static func <(lhs: WalletInfoListEntry, rhs: WalletInfoListEntry) -> Bool {
return lhs.index < rhs.index
}
func item(theme: PresentationTheme, action: @escaping (WalletTransaction) -> Void) -> ListViewItem {
let item = self.item
return WalletInfoTransactionItem(theme: theme, walletTransaction: self.item, action: {
action(item)
})
}
}
private func preparedTransition(from fromEntries: [WalletInfoListEntry], to toEntries: [WalletInfoListEntry], presentationData: PresentationData, action: @escaping (WalletTransaction) -> Void) -> WalletInfoListTransaction {
let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries)
let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) }
let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(theme: presentationData.theme, action: action), directionHint: nil) }
let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(theme: presentationData.theme, action: action), directionHint: nil) }
return WalletInfoListTransaction(deletions: deletions, insertions: insertions, updates: updates)
}
private final class WalletInfoScreenNode: ViewControllerTracingNode {
private let account: Account
private let tonContext: TonContext
private var presentationData: PresentationData
private let walletInfo: WalletInfo
private let address: String
private let openTransaction: (WalletTransaction) -> Void
private let headerNode: WalletInfoHeaderNode
private let listNode: ListView
private let emptyNode: WalletInfoEmptyNode
private var enqueuedTransactions: [WalletInfoListTransaction] = []
private var validLayout: (ContainerViewLayout, CGFloat)?
private let balanceDisposable = MetaDisposable()
private let transactionListDisposable = MetaDisposable()
init(account: Account, tonContext: TonContext, presentationData: PresentationData, walletInfo: WalletInfo, address: String, exportAction: @escaping () -> Void) {
private var listOffset: CGFloat?
private var loadingMoreTransactions: Bool = false
private var canLoadMoreTransactions: Bool = true
private var currentEntries: [WalletInfoListEntry]?
init(account: Account, tonContext: TonContext, presentationData: PresentationData, walletInfo: WalletInfo, address: String, sendAction: @escaping () -> Void, receiveAction: @escaping () -> Void, openTransaction: @escaping (WalletTransaction) -> Void) {
self.account = account
self.tonContext = tonContext
self.presentationData = presentationData
self.walletInfo = walletInfo
self.exportAction = exportAction
self.address = address
self.openTransaction = openTransaction
let addressString = Data(base64Encoded: walletInfo.publicKey.rawValue).flatMap({ String(data: $0, encoding: .utf8) }) ?? ""
self.headerNode = WalletInfoHeaderNode(theme: presentationData.theme, sendAction: sendAction, receiveAction: receiveAction)
let title: String = address
let text: String = "..."
let buttonText: String = "Export"
self.listNode = ListView()
self.listNode.verticalScrollIndicatorColor = self.presentationData.theme.list.scrollIndicatorColor
self.listNode.verticalScrollIndicatorFollowsOverscroll = true
self.titleNode = ImmediateTextNode()
self.titleNode.displaysAsynchronously = false
self.titleNode.attributedText = NSAttributedString(string: title, font: Font.bold(32.0), textColor: self.presentationData.theme.list.itemPrimaryTextColor)
self.titleNode.maximumNumberOfLines = 0
self.titleNode.textAlignment = .center
self.textNode = ImmediateTextNode()
self.textNode.displaysAsynchronously = false
self.textNode.attributedText = NSAttributedString(string: text, font: Font.regular(16.0), textColor: self.presentationData.theme.list.itemPrimaryTextColor)
self.textNode.maximumNumberOfLines = 0
self.textNode.textAlignment = .center
self.buttonNode = SolidRoundedButtonNode(title: buttonText, theme: self.presentationData.theme, height: 50.0, cornerRadius: 10.0, gloss: true)
self.emptyNode = WalletInfoEmptyNode(presentationData: self.presentationData, address: self.address)
self.emptyNode.isHidden = true
super.init()
self.backgroundColor = self.presentationData.theme.list.plainBackgroundColor
self.addSubnode(self.titleNode)
self.addSubnode(self.textNode)
self.addSubnode(self.buttonNode)
self.buttonNode.pressed = {
exportAction()
}
self.backgroundColor = .white
self.balanceDisposable.set((currentWalletBalance(publicKey: walletInfo.publicKey, tonInstance: tonContext.instance)
|> deliverOnMainQueue).start(next: { [weak self] value in
guard let strongSelf = self else {
return
}
strongSelf.textNode.attributedText = NSAttributedString(string: "\(value)", font: Font.regular(16.0), textColor: strongSelf.presentationData.theme.list.itemPrimaryTextColor)
let firstTime = strongSelf.headerNode.balance == nil
strongSelf.headerNode.balanceNode.balance = formatBalanceText(max(0, value.rawValue))
strongSelf.headerNode.balance = max(0, value.rawValue)
if let (layout, navigationHeight) = strongSelf.validLayout {
strongSelf.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: .immediate)
}
if firstTime {
strongSelf.headerNode.animateIn()
}
}))
self.addSubnode(self.listNode)
self.addSubnode(self.emptyNode)
self.addSubnode(self.headerNode)
self.listNode.updateFloatingHeaderOffset = { [weak self] offset, listTransition in
guard let strongSelf = self, let (layout, navigationHeight) = strongSelf.validLayout else {
return
}
strongSelf.listOffset = offset
strongSelf.headerNode.update(size: strongSelf.headerNode.bounds.size, navigationHeight: navigationHeight, offset: offset, transition: listTransition)
}
self.listNode.visibleBottomContentOffsetChanged = { [weak self] offset in
guard let strongSelf = self else {
return
}
guard case let .known(value) = offset, value < 100.0 else {
return
}
if !strongSelf.loadingMoreTransactions && strongSelf.canLoadMoreTransactions {
strongSelf.loadMoreTransactions()
}
}
self.listNode.didEndScrolling = { [weak self] in
guard let strongSelf = self, let (_, navigationHeight) = strongSelf.validLayout else {
return
}
switch strongSelf.listNode.visibleContentOffset() {
case let .known(offset):
if offset < strongSelf.listNode.insets.top {
/*if offset > strongSelf.listNode.insets.top / 2.0 {
strongSelf.scrollToHideHeader()
} else {
strongSelf.scrollToTop()
}*/
}
default:
break
}
}
self.refreshTransactions()
}
func scrollToHideHeader() {
guard let (_, navigationHeight) = self.validLayout else {
return
}
self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: ListViewScrollToItem(index: 0, position: .top(navigationHeight), animated: true, curve: .Spring(duration: 0.4), directionHint: .Up), updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in })
}
func scrollToTop() {
self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: ListViewScrollToItem(index: 0, position: .top(0.0), animated: true, curve: .Spring(duration: 0.4), directionHint: .Up), updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in })
}
func containerLayoutUpdated(layout: ContainerViewLayout, navigationHeight: CGFloat, transition: ContainedViewLayoutTransition) {
let isFirstLayout = self.validLayout == nil
self.validLayout = (layout, navigationHeight)
let headerHeight: CGFloat = navigationHeight + 260.0
let topInset: CGFloat = headerHeight
let headerFrame = CGRect(origin: CGPoint(), size: CGSize(width: layout.size.width, height: headerHeight))
transition.updateFrame(node: self.headerNode, frame: headerFrame)
self.headerNode.update(size: headerFrame.size, navigationHeight: navigationHeight, offset: self.listOffset ?? 0.0, transition: transition)
transition.updateFrame(node: self.listNode, frame: CGRect(origin: CGPoint(), size: layout.size))
var duration: Double = 0.0
var curve: UInt = 0
switch transition {
case .immediate:
break
case let .animated(animationDuration, animationCurve):
duration = animationDuration
switch animationCurve {
case .easeInOut, .custom:
break
case .spring:
curve = 7
}
}
let listViewCurve: ListViewAnimationCurve
if curve == 7 {
listViewCurve = .Spring(duration: duration)
} else {
listViewCurve = .Default(duration: duration)
}
self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: nil, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: layout.size, insets: UIEdgeInsets(top: topInset, left: 0.0, bottom: layout.intrinsicInsets.bottom, right: 0.0), scrollIndicatorInsets: UIEdgeInsets(top: topInset + 3.0, left: 0.0, bottom: layout.intrinsicInsets.bottom, right: 0.0), duration: duration, curve: listViewCurve), stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in })
let emptyNodeHeight = self.emptyNode.updateLayout(width: layout.size.width, transition: transition)
let maxEmptyNodeHeight: CGFloat = max(100.0, layout.size.height - headerHeight)
let emptyNodeFrame = CGRect(origin: CGPoint(x: 0.0, y: headerHeight + floor((maxEmptyNodeHeight - emptyNodeHeight) / 2.0)), size: CGSize(width: layout.size.width, height: emptyNodeHeight))
transition.updateFrame(node: self.emptyNode, frame: emptyNodeFrame)
if isFirstLayout {
while !self.enqueuedTransactions.isEmpty {
self.dequeueTransaction()
}
}
}
private func refreshTransactions() {
self.transactionListDisposable.set(nil)
self.loadingMoreTransactions = true
self.transactionListDisposable.set((getWalletTransactions(address: self.address, previousId: nil, tonInstance: self.tonContext.instance)
|> deliverOnMainQueue).start(next: { [weak self] transactions in
guard let strongSelf = self else {
return
}
strongSelf.transactionsLoaded(isReload: true, transactions: transactions)
}, error: { [weak self] _ in
guard let strongSelf = self else {
return
}
}))
}
func containerLayoutUpdated(layout: ContainerViewLayout, navigationHeight: CGFloat, transition: ContainedViewLayoutTransition) {
self.validLayout = (layout, navigationHeight)
private func loadMoreTransactions() {
if self.loadingMoreTransactions {
return
}
self.loadingMoreTransactions = true
self.transactionListDisposable.set((getWalletTransactions(address: self.address, previousId: self.currentEntries?.last?.item.previousTransactionId, tonInstance: self.tonContext.instance)
|> deliverOnMainQueue).start(next: { [weak self] transactions in
guard let strongSelf = self else {
return
}
strongSelf.transactionsLoaded(isReload: false, transactions: transactions)
}, error: { [weak self] _ in
guard let strongSelf = self else {
return
}
}))
}
private func transactionsLoaded(isReload: Bool, transactions: [WalletTransaction]) {
self.loadingMoreTransactions = false
self.canLoadMoreTransactions = transactions.count > 2
let sideInset: CGFloat = 32.0
let buttonSideInset: CGFloat = 48.0
let titleSpacing: CGFloat = 10.0
let termsSpacing: CGFloat = 10.0
let buttonHeight: CGFloat = 50.0
let isFirst = self.currentEntries == nil
let titleSize = self.titleNode.updateLayout(CGSize(width: layout.size.width - sideInset * 2.0, height: layout.size.height))
let textSize = self.textNode.updateLayout(CGSize(width: layout.size.width - sideInset * 2.0, height: layout.size.height))
var updatedEntries: [WalletInfoListEntry] = []
if isReload {
for transaction in transactions {
updatedEntries.append(WalletInfoListEntry(index: updatedEntries.count, item: transaction))
}
} else {
updatedEntries = self.currentEntries ?? []
var existingIds = Set(updatedEntries.map { $0.item.previousTransactionId })
for transaction in transactions {
if !existingIds.contains(transaction.previousTransactionId) {
existingIds.insert(transaction.previousTransactionId)
updatedEntries.append(WalletInfoListEntry(index: updatedEntries.count, item: transaction))
}
}
}
let contentHeight = titleSize.height + titleSpacing + textSize.height
let contentVerticalOrigin = floor((layout.size.height - contentHeight) / 2.0)
let transaction = preparedTransition(from: self.currentEntries ?? [], to: updatedEntries, presentationData: self.presentationData, action: { [weak self] transaction in
guard let strongSelf = self else {
return
}
strongSelf.openTransaction(transaction)
})
self.currentEntries = updatedEntries
let titleFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - titleSize.width) / 2.0), y: contentVerticalOrigin), size: titleSize)
transition.updateFrameAdditive(node: self.titleNode, frame: titleFrame)
let textFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - textSize.width) / 2.0), y: titleFrame.maxY + titleSpacing), size: textSize)
transition.updateFrameAdditive(node: self.textNode, frame: textFrame)
self.enqueuedTransactions.append(transaction)
self.dequeueTransaction()
let minimalBottomInset: CGFloat = 60.0
let bottomInset = layout.intrinsicInsets.bottom + minimalBottomInset
if updatedEntries.isEmpty {
self.emptyNode.isHidden = false
} else {
self.emptyNode.isHidden = true
}
let buttonWidth = layout.size.width - buttonSideInset * 2.0
if isFirst {
if !updatedEntries.isEmpty {
self.emptyNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
} else {
self.listNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
}
}
}
private func dequeueTransaction() {
guard let layout = self.validLayout, let transaction = self.enqueuedTransactions.first else {
return
}
let buttonFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - buttonWidth) / 2.0), y: layout.size.height - bottomInset - buttonHeight), size: CGSize(width: buttonWidth, height: buttonHeight))
transition.updateFrame(node: self.buttonNode, frame: buttonFrame)
self.buttonNode.updateLayout(width: buttonFrame.width, transition: transition)
self.enqueuedTransactions.remove(at: 0)
var options = ListViewDeleteAndInsertOptions()
options.insert(.Synchronous)
options.insert(.PreferSynchronousResourceLoading)
options.insert(.PreferSynchronousDrawing)
self.listNode.transaction(deleteIndices: transaction.deletions, insertIndicesAndItems: transaction.insertions, updateIndicesAndItems: transaction.updates, options: options, updateSizeAndInsets: nil, updateOpaqueState: nil, completion: { _ in
})
}
}
func formatBalanceText(_ value: Int64) -> String {
var balanceText = "\(value)"
while balanceText.count < 10 {
balanceText.insert("0", at: balanceText.startIndex)
}
balanceText.insert(".", at: balanceText.index(balanceText.endIndex, offsetBy: -9))
while true {
if balanceText.hasSuffix("0") {
if balanceText.hasSuffix(".0") {
break
} else {
balanceText.removeLast()
}
} else {
break
}
}
return balanceText
}

View File

@ -0,0 +1,306 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import TelegramPresentationData
import ItemListUI
import TelegramCore
private let transactionIcon = UIImage(bundleImageName: "Wallet/TransactionGem")?.precomposed()
class WalletInfoTransactionItem: ListViewItem {
let theme: PresentationTheme
let walletTransaction: WalletTransaction
let action: () -> Void
init(theme: PresentationTheme, walletTransaction: WalletTransaction, action: @escaping () -> Void) {
self.theme = theme
self.walletTransaction = walletTransaction
self.action = action
}
func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
async {
let node = WalletInfoTransactionItemNode()
let (layout, apply) = node.asyncLayout()(self, params, previousItem != nil, nextItem != nil)
node.contentSize = layout.contentSize
node.insets = layout.insets
Queue.mainQueue().async {
completion(node, {
return (nil, { _ in apply() })
})
}
}
}
func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) {
Queue.mainQueue().async {
if let nodeValue = node() as? WalletInfoTransactionItemNode {
let makeLayout = nodeValue.asyncLayout()
async {
let (layout, apply) = makeLayout(self, params, previousItem != nil, nextItem != nil)
Queue.mainQueue().async {
completion(layout, { _ in
apply()
})
}
}
}
}
}
var selectable: Bool = true
func selected(listView: ListView){
listView.clearHighlightAnimated(true)
self.action()
}
}
private let titleFont = Font.medium(17.0)
private let textFont = Font.monospace(15.0)
private let dateFont = Font.regular(14.0)
private let directionFont = Font.regular(15.0)
class WalletInfoTransactionItemNode: ListViewItemNode {
private let backgroundNode: ASDisplayNode
private let topStripeNode: ASDisplayNode
private let bottomStripeNode: ASDisplayNode
private let highlightedBackgroundNode: ASDisplayNode
private let titleNode: TextNode
private let directionNode: TextNode
private let iconNode: ASImageNode
private let textNode: TextNode
private let dateNode: TextNode
private let activateArea: AccessibilityAreaNode
private var item: WalletInfoTransactionItem?
init() {
self.backgroundNode = ASDisplayNode()
self.backgroundNode.isLayerBacked = true
self.backgroundNode.backgroundColor = .white
self.topStripeNode = ASDisplayNode()
self.topStripeNode.isLayerBacked = true
self.bottomStripeNode = ASDisplayNode()
self.bottomStripeNode.isLayerBacked = true
self.titleNode = TextNode()
self.titleNode.isUserInteractionEnabled = false
self.titleNode.contentMode = .left
self.titleNode.contentsScale = UIScreen.main.scale
self.directionNode = TextNode()
self.directionNode.isUserInteractionEnabled = false
self.directionNode.contentMode = .left
self.directionNode.contentsScale = UIScreen.main.scale
self.iconNode = ASImageNode()
self.iconNode.displaysAsynchronously = false
self.iconNode.displayWithoutProcessing = true
self.textNode = TextNode()
self.textNode.isUserInteractionEnabled = false
self.textNode.contentMode = .left
self.textNode.contentsScale = UIScreen.main.scale
self.dateNode = TextNode()
self.dateNode.isUserInteractionEnabled = false
self.dateNode.contentMode = .left
self.dateNode.contentsScale = UIScreen.main.scale
self.highlightedBackgroundNode = ASDisplayNode()
self.highlightedBackgroundNode.isLayerBacked = true
self.activateArea = AccessibilityAreaNode()
super.init(layerBacked: false, dynamicBounce: false)
self.addSubnode(self.titleNode)
self.addSubnode(self.iconNode)
self.addSubnode(self.directionNode)
self.addSubnode(self.textNode)
self.addSubnode(self.dateNode)
self.addSubnode(self.activateArea)
}
func asyncLayout() -> (_ item: WalletInfoTransactionItem, _ params: ListViewItemLayoutParams, _ hasPrevious: Bool, _ hasNext: Bool) -> (ListViewItemNodeLayout, () -> Void) {
let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
let makeDirectionLayout = TextNode.asyncLayout(self.directionNode)
let makeTextLayout = TextNode.asyncLayout(self.textNode)
let makeDateLayout = TextNode.asyncLayout(self.dateNode)
let currentItem = self.item
return { item, params, hasPrevious, hasNext in
var updatedTheme: PresentationTheme?
if currentItem?.theme !== item.theme {
updatedTheme = item.theme
}
let iconImage: UIImage? = transactionIcon
let iconSize = iconImage?.size ?? CGSize(width: 10.0, height: 10.0)
let leftInset = 16.0 + params.leftInset
let title: String
let directionText: String
let titleColor: UIColor
let transferredValue = item.walletTransaction.transferredValue
var text: String = ""
if transferredValue <= 0 {
title = "\(formatBalanceText(transferredValue))"
titleColor = item.theme.list.itemPrimaryTextColor
if item.walletTransaction.outMessages.isEmpty {
directionText = ""
text = "Empty Transaction"
} else {
directionText = "to"
for message in item.walletTransaction.outMessages {
if !text.isEmpty {
text.append("\n")
}
text.append(message.destination)
}
}
} else {
title = "+\(formatBalanceText(transferredValue))"
titleColor = item.theme.chatList.secretTitleColor
directionText = "from"
if let inMessage = item.walletTransaction.inMessage {
text = inMessage.source
} else {
text = "<unknown>"
}
}
let dateText = " "
let (dateLayout, dateApply) = makeDateLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: dateText, font: dateFont, textColor: item.theme.list.itemSecondaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - leftInset - 20.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let (directionLayout, directionApply) = makeDirectionLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: directionText, font: directionFont, textColor: item.theme.list.itemSecondaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - leftInset - 20.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: title, font: titleFont, textColor: titleColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - leftInset - 20.0 - dateLayout.size.width - directionLayout.size.width - iconSize.width, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let (textLayout, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: text, font: textFont, textColor: item.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - leftInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let contentSize: CGSize
let insets: UIEdgeInsets
let separatorHeight = UIScreenPixel
let itemBackgroundColor: UIColor
let itemSeparatorColor: UIColor
itemBackgroundColor = item.theme.list.plainBackgroundColor
itemSeparatorColor = item.theme.list.itemPlainSeparatorColor
let topInset: CGFloat = 11.0
let bottomInset: CGFloat = 11.0
let titleSpacing: CGFloat = 2.0
contentSize = CGSize(width: params.width, height: topInset + titleLayout.size.height + titleSpacing + textLayout.size.height + bottomInset)
insets = UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 0.0)
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
return (layout, { [weak self] in
if let strongSelf = self {
strongSelf.item = item
strongSelf.activateArea.frame = CGRect(origin: CGPoint(x: params.leftInset, y: 0.0), size: CGSize(width: params.width - params.leftInset - params.rightInset, height: layout.contentSize.height))
//strongSelf.activateArea.accessibilityLabel = item.title
if let _ = updatedTheme {
strongSelf.topStripeNode.backgroundColor = itemSeparatorColor
strongSelf.bottomStripeNode.backgroundColor = itemSeparatorColor
strongSelf.backgroundNode.backgroundColor = itemBackgroundColor
strongSelf.highlightedBackgroundNode.backgroundColor = item.theme.list.itemHighlightedBackgroundColor
strongSelf.iconNode.image = iconImage
}
let _ = titleApply()
let _ = textApply()
let _ = dateApply()
let _ = directionApply()
if strongSelf.backgroundNode.supernode != nil {
strongSelf.backgroundNode.removeFromSupernode()
}
if strongSelf.topStripeNode.supernode != nil {
strongSelf.topStripeNode.removeFromSupernode()
}
if strongSelf.bottomStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 0)
}
strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: leftInset, y: contentSize.height - separatorHeight), size: CGSize(width: params.width - leftInset, height: separatorHeight))
let titleFrame = CGRect(origin: CGPoint(x: leftInset, y: topInset), size: titleLayout.size)
strongSelf.titleNode.frame = titleFrame
let iconFrame = CGRect(origin: CGPoint(x: titleFrame.maxX + 3.0, y: titleFrame.minY + floor((titleFrame.height - iconSize.height) / 2.0) - 1.0), size: iconSize)
strongSelf.iconNode.frame = iconFrame
let directionFrame = CGRect(origin: CGPoint(x: iconFrame.maxX + 3.0, y: titleFrame.maxY - directionLayout.size.height - 1.0), size: directionLayout.size)
strongSelf.directionNode.frame = directionFrame
strongSelf.textNode.frame = CGRect(origin: CGPoint(x: leftInset, y: titleFrame.maxY + titleSpacing), size: textLayout.size)
strongSelf.dateNode.frame = CGRect(origin: CGPoint(x: params.width - leftInset - dateLayout.size.width, y: topInset), size: dateLayout.size)
strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: params.width, height: layout.contentSize.height + UIScreenPixel * 2.0))
}
})
}
}
override func setHighlighted(_ highlighted: Bool, at point: CGPoint, animated: Bool) {
super.setHighlighted(highlighted, at: point, animated: animated)
if highlighted {
self.highlightedBackgroundNode.alpha = 1.0
if self.highlightedBackgroundNode.supernode == nil {
var anchorNode: ASDisplayNode?
if self.bottomStripeNode.supernode != nil {
anchorNode = self.bottomStripeNode
} else if self.topStripeNode.supernode != nil {
anchorNode = self.topStripeNode
} else if self.backgroundNode.supernode != nil {
anchorNode = self.backgroundNode
}
if let anchorNode = anchorNode {
self.insertSubnode(self.highlightedBackgroundNode, aboveSubnode: anchorNode)
} else {
self.addSubnode(self.highlightedBackgroundNode)
}
}
} else {
if self.highlightedBackgroundNode.supernode != nil {
if animated {
self.highlightedBackgroundNode.layer.animateAlpha(from: self.highlightedBackgroundNode.alpha, to: 0.0, duration: 0.4, completion: { [weak self] completed in
if let strongSelf = self {
if completed {
strongSelf.highlightedBackgroundNode.removeFromSupernode()
}
}
})
self.highlightedBackgroundNode.alpha = 0.0
} else {
self.highlightedBackgroundNode.removeFromSupernode()
}
}
}
}
override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
}
override func animateRemoved(_ currentTimestamp: Double, duration: Double) {
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
}
}

View File

@ -0,0 +1,163 @@
import Foundation
import UIKit
import AppBundle
import AccountContext
import TelegramPresentationData
import AsyncDisplayKit
import Display
import Postbox
import TelegramCore
import SolidRoundedButtonNode
import AnimationUI
import SwiftSignalKit
import OverlayStatusController
import ItemListUI
private final class WalletSettingsControllerArguments {
let exportWallet: () -> Void
let deleteWallet: () -> Void
init(exportWallet: @escaping () -> Void, deleteWallet: @escaping () -> Void) {
self.exportWallet = exportWallet
self.deleteWallet = deleteWallet
}
}
private enum WalletSettingsSection: Int32 {
case exportWallet
case deleteWallet
}
private enum WalletSettingsEntry: ItemListNodeEntry {
case exportWallet(PresentationTheme, String)
case deleteWallet(PresentationTheme, String)
var section: ItemListSectionId {
switch self {
case .exportWallet:
return WalletSettingsSection.exportWallet.rawValue
case .deleteWallet:
return WalletSettingsSection.deleteWallet.rawValue
}
}
var stableId: Int32 {
switch self {
case .exportWallet:
return 0
case .deleteWallet:
return 1
}
}
static func <(lhs: WalletSettingsEntry, rhs: WalletSettingsEntry) -> Bool {
return lhs.stableId < rhs.stableId
}
func item(_ arguments: WalletSettingsControllerArguments) -> ListViewItem {
switch self {
case let .exportWallet(theme, text):
return ItemListActionItem(theme: theme, title: text, kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: {
arguments.exportWallet()
})
case let .deleteWallet(theme, text):
return ItemListActionItem(theme: theme, title: text, kind: .destructive, alignment: .natural, sectionId: self.section, style: .blocks, action: {
arguments.deleteWallet()
})
}
}
}
private struct WalletSettingsControllerState: Equatable {
}
private func walletSettingsControllerEntries(presentationData: PresentationData, state: WalletSettingsControllerState) -> [WalletSettingsEntry] {
var entries: [WalletSettingsEntry] = []
entries.append(.exportWallet(presentationData.theme, "Export Wallet"))
entries.append(.deleteWallet(presentationData.theme, "Delete Wallet"))
return entries
}
public func walletSettingsController(context: AccountContext, tonContext: TonContext, walletInfo: WalletInfo) -> ViewController {
let statePromise = ValuePromise(WalletSettingsControllerState(), ignoreRepeated: true)
let stateValue = Atomic(value: WalletSettingsControllerState())
let updateState: ((WalletSettingsControllerState) -> WalletSettingsControllerState) -> Void = { f in
statePromise.set(stateValue.modify { f($0) })
}
var dismissImpl: (() -> Void)?
var presentControllerImpl: ((ViewController, Any?) -> Void)?
var pushControllerImpl: ((ViewController) -> Void)?
var replaceAllWalletControllersImpl: ((ViewController) -> Void)?
let arguments = WalletSettingsControllerArguments(exportWallet: {
let _ = (walletRestoreWords(network: context.account.network, walletInfo: walletInfo, tonInstance: tonContext.instance, keychain: tonContext.keychain)
|> deliverOnMainQueue).start(next: { wordList in
pushControllerImpl?(WalletWordDisplayScreen(context: context, tonContext: tonContext, walletInfo: walletInfo, wordList: wordList))
})
}, deleteWallet: {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let actionSheet = ActionSheetController(presentationTheme: presentationData.theme)
actionSheet.setItemGroups([ActionSheetItemGroup(items: [
ActionSheetButtonItem(title: "Delete Wallet", color: .destructive, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
let _ = (debugDeleteWallets(postbox: context.account.postbox)
|> deliverOnMainQueue).start(completed: {
if let tonContext = context.tonContext {
replaceAllWalletControllersImpl?(WalletSplashScreen(context: context, tonContext: tonContext, mode: .intro))
}
})
})
]), ActionSheetItemGroup(items: [
ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
})
])])
presentControllerImpl?(actionSheet, nil)
})
let signal = combineLatest(queue: .mainQueue(), context.sharedContext.presentationData, statePromise.get())
|> map { presentationData, state -> (ItemListControllerState, (ItemListNodeState<WalletSettingsEntry>, WalletSettingsEntry.ItemGenerationArguments)) in
let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text("Wallet Settings"), leftNavigationButton: nil, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false)
let listState = ItemListNodeState(entries: walletSettingsControllerEntries(presentationData: presentationData, state: state), style: .blocks, animateChanges: false)
return (controllerState, (listState, arguments))
}
|> afterDisposed {
}
let controller = ItemListController(context: context, state: signal)
controller.navigationPresentation = .modal
controller.enableInteractiveDismiss = true
dismissImpl = { [weak controller] in
controller?.view.endEditing(true)
controller?.dismiss()
}
presentControllerImpl = { [weak controller] c, a in
controller?.present(c, in: .window(.root), with: a)
}
pushControllerImpl = { [weak controller] c in
controller?.push(c)
}
replaceAllWalletControllersImpl = { [weak controller] c in
if let navigationController = controller?.navigationController as? NavigationController {
var controllers = navigationController.viewControllers
controllers = controllers.filter { listController in
if listController === controller {
return false
}
if listController is WalletInfoScreen {
return false
}
return true
}
controllers.append(c)
navigationController.setViewControllers(controllers, animated: true)
}
}
return controller
}

View File

@ -247,7 +247,7 @@ private final class WalletSplashScreenNode: ViewControllerTracingNode {
self.secondaryActionButtonNode = HighlightTrackingButtonNode()
self.buttonNode = SolidRoundedButtonNode(title: buttonText, theme: self.presentationData.theme, height: 50.0, cornerRadius: 10.0, gloss: true)
self.buttonNode = SolidRoundedButtonNode(title: buttonText, theme: SolidRoundedButtonTheme(theme: self.presentationData.theme), height: 50.0, cornerRadius: 10.0, gloss: true)
super.init()

View File

@ -0,0 +1,310 @@
import Foundation
import UIKit
import AppBundle
import AccountContext
import TelegramPresentationData
import AsyncDisplayKit
import Display
import Postbox
import TelegramCore
import SolidRoundedButtonNode
import AnimationUI
import SwiftSignalKit
import OverlayStatusController
import ItemListUI
private final class WalletTransactionInfoControllerArguments {
let copyWalletAddress: () -> Void
let sendGrams: () -> Void
init(copyWalletAddress: @escaping () -> Void, sendGrams: @escaping () -> Void) {
self.copyWalletAddress = copyWalletAddress
self.sendGrams = sendGrams
}
}
private enum WalletTransactionInfoSection: Int32 {
case amount
case info
}
private enum WalletTransactionInfoEntry: ItemListNodeEntry {
case amount(PresentationTheme, WalletTransaction)
case infoHeader(PresentationTheme, String)
case infoAddress(PresentationTheme, String)
case infoCopyAddress(PresentationTheme, String)
case infoSendGrams(PresentationTheme, String)
var section: ItemListSectionId {
switch self {
case .amount:
return WalletTransactionInfoSection.amount.rawValue
case .infoHeader, .infoAddress, .infoCopyAddress, .infoSendGrams:
return WalletTransactionInfoSection.info.rawValue
}
}
var stableId: Int32 {
switch self {
case .amount:
return 0
case .infoHeader:
return 1
case .infoAddress:
return 2
case .infoCopyAddress:
return 3
case .infoSendGrams:
return 4
}
}
static func <(lhs: WalletTransactionInfoEntry, rhs: WalletTransactionInfoEntry) -> Bool {
return lhs.stableId < rhs.stableId
}
func item(_ arguments: WalletTransactionInfoControllerArguments) -> ListViewItem {
switch self {
case let .amount(theme, walletTransaction):
return WalletTransactionHeaderItem(theme: theme, walletTransaction: walletTransaction, sectionId: self.section)
case let .infoHeader(theme, text):
return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section)
case let .infoAddress(theme, text):
return ItemListMultilineTextItem(theme: theme, text: text, enabledEntityTypes: [], sectionId: self.section, style: .blocks)
case let .infoCopyAddress(theme, text):
return ItemListActionItem(theme: theme, title: text, kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: {
arguments.copyWalletAddress()
})
case let .infoSendGrams(theme, text):
return ItemListActionItem(theme: theme, title: text, kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: {
arguments.sendGrams()
})
}
}
}
private struct WalletTransactionInfoControllerState: Equatable {
}
private func extractAddress(_ walletTransaction: WalletTransaction) -> String {
let transferredValue = walletTransaction.transferredValue
var text = ""
if transferredValue <= 0 {
if walletTransaction.outMessages.isEmpty {
text = "No Address"
} else {
for message in walletTransaction.outMessages {
if !text.isEmpty {
text.append("\n\n")
}
text.append(message.destination)
}
}
} else {
if let inMessage = walletTransaction.inMessage {
text = inMessage.source
} else {
text = "<unknown>"
}
}
return text
}
private func walletTransactionInfoControllerEntries(presentationData: PresentationData, walletTransaction: WalletTransaction, state: WalletTransactionInfoControllerState) -> [WalletTransactionInfoEntry] {
var entries: [WalletTransactionInfoEntry] = []
entries.append(.amount(presentationData.theme, walletTransaction))
let transferredValue = walletTransaction.transferredValue
let text = extractAddress(walletTransaction)
if transferredValue <= 0 {
entries.append(.infoHeader(presentationData.theme, "RECIPIENT"))
} else {
entries.append(.infoHeader(presentationData.theme, "SENDER"))
}
entries.append(.infoAddress(presentationData.theme, text))
entries.append(.infoCopyAddress(presentationData.theme, "Copy Address"))
entries.append(.infoSendGrams(presentationData.theme, "Send Grams"))
return entries
}
func walletTransactionInfoController(context: AccountContext, walletTransaction: WalletTransaction) -> ViewController {
let statePromise = ValuePromise(WalletTransactionInfoControllerState(), ignoreRepeated: true)
let stateValue = Atomic(value: WalletTransactionInfoControllerState())
let updateState: ((WalletTransactionInfoControllerState) -> WalletTransactionInfoControllerState) -> Void = { f in
statePromise.set(stateValue.modify { f($0) })
}
var dismissImpl: (() -> Void)?
var presentControllerImpl: ((ViewController, Any?) -> Void)?
let arguments = WalletTransactionInfoControllerArguments(copyWalletAddress: {
let address = extractAddress(walletTransaction)
UIPasteboard.general.string = address
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
presentControllerImpl?(OverlayStatusController(theme: presentationData.theme, strings: presentationData.strings, type: .success), nil)
}, sendGrams: {
})
let signal = combineLatest(queue: .mainQueue(), context.sharedContext.presentationData, statePromise.get())
|> map { presentationData, state -> (ItemListControllerState, (ItemListNodeState<WalletTransactionInfoEntry>, WalletTransactionInfoEntry.ItemGenerationArguments)) in
let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text("Transaction"), leftNavigationButton: nil, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false)
let listState = ItemListNodeState(entries: walletTransactionInfoControllerEntries(presentationData: presentationData, walletTransaction: walletTransaction, state: state), style: .blocks, animateChanges: false)
return (controllerState, (listState, arguments))
}
|> afterDisposed {
}
let controller = ItemListController(context: context, state: signal)
controller.navigationPresentation = .modal
controller.enableInteractiveDismiss = true
dismissImpl = { [weak controller] in
controller?.view.endEditing(true)
controller?.dismiss()
}
presentControllerImpl = { [weak controller] c, a in
controller?.present(c, in: .window(.root), with: a)
}
return controller
}
class WalletTransactionHeaderItem: ListViewItem, ItemListItem {
let theme: PresentationTheme
let walletTransaction: WalletTransaction
let sectionId: ItemListSectionId
let isAlwaysPlain: Bool = true
init(theme: PresentationTheme, walletTransaction: WalletTransaction, sectionId: ItemListSectionId) {
self.theme = theme
self.walletTransaction = walletTransaction
self.sectionId = sectionId
}
func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
async {
let node = WalletTransactionHeaderItemNode()
let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
node.contentSize = layout.contentSize
node.insets = layout.insets
Queue.mainQueue().async {
completion(node, {
return (nil, { _ in apply() })
})
}
}
}
func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) {
Queue.mainQueue().async {
guard let nodeValue = node() as? WalletTransactionHeaderItemNode else {
assertionFailure()
return
}
let makeLayout = nodeValue.asyncLayout()
async {
let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
Queue.mainQueue().async {
completion(layout, { _ in
apply()
})
}
}
}
}
}
private let titleFont = Font.regular(14.0)
private let titleBoldFont = Font.semibold(14.0)
private class WalletTransactionHeaderItemNode: ListViewItemNode {
private let titleNode: TextNode
private let iconNode: ASImageNode
private let activateArea: AccessibilityAreaNode
private var item: WalletTransactionHeaderItem?
init() {
self.titleNode = TextNode()
self.titleNode.isUserInteractionEnabled = false
self.titleNode.contentMode = .left
self.titleNode.contentsScale = UIScreen.main.scale
self.iconNode = ASImageNode()
self.iconNode.displaysAsynchronously = false
self.iconNode.displayWithoutProcessing = true
self.iconNode.image = UIImage(bundleImageName: "Wallet/BalanceGem")?.precomposed()
self.activateArea = AccessibilityAreaNode()
self.activateArea.accessibilityTraits = .staticText
super.init(layerBacked: false, dynamicBounce: false)
self.addSubnode(self.titleNode)
self.addSubnode(self.iconNode)
self.addSubnode(self.activateArea)
}
func asyncLayout() -> (_ item: WalletTransactionHeaderItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) {
let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
let iconSize = self.iconNode.image?.size ?? CGSize(width: 10.0, height: 10.0)
return { item, params, neighbors in
let leftInset: CGFloat = 15.0 + params.leftInset
let verticalInset: CGFloat = 24.0
let title: String
let titleColor: UIColor
let transferredValue = item.walletTransaction.transferredValue
if transferredValue <= 0 {
title = "\(formatBalanceText(transferredValue))"
titleColor = item.theme.list.itemPrimaryTextColor
} else {
title = "+\(formatBalanceText(transferredValue))"
titleColor = item.theme.chatList.secretTitleColor
}
let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: title, font: Font.semibold(39.0), textColor: titleColor), backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: params.width - params.rightInset - leftInset * 2.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let contentSize: CGSize
contentSize = CGSize(width: params.width, height: titleLayout.size.height + verticalInset + verticalInset)
let insets = itemListNeighborsGroupedInsets(neighbors)
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
return (layout, { [weak self] in
if let strongSelf = self {
strongSelf.item = item
strongSelf.activateArea.frame = CGRect(origin: CGPoint(x: params.leftInset, y: 0.0), size: CGSize(width: params.width - params.leftInset - params.rightInset, height: layout.contentSize.height))
//strongSelf.activateArea.accessibilityLabel = attributedText.string
let _ = titleApply()
let iconSpacing: CGFloat = 8.0
let contentWidth = titleLayout.size.width + iconSpacing + iconSize.width / 2.0
let titleFrame = CGRect(origin: CGPoint(x: floor((params.width - contentWidth) / 2.0), y: verticalInset), size: titleLayout.size)
strongSelf.titleNode.frame = titleFrame
strongSelf.iconNode.frame = CGRect(origin: CGPoint(x: titleFrame.maxX + iconSpacing, y: titleFrame.minY + floor((titleFrame.height - iconSize.height) / 2.0)), size: iconSize)
}
})
}
}
override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
}
override func animateRemoved(_ currentTimestamp: Double, duration: Double) {
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
}
}

View File

@ -2244,6 +2244,7 @@ private func generateClearIcon(color: UIColor) -> UIImage? {
private final class WordCheckInputNode: ASDisplayNode, UITextFieldDelegate {
private let next: (WordCheckInputNode) -> Void
private let focused: (WordCheckInputNode) -> Void
private let pasteWords: ([String]) -> Void
private let backgroundNode: ASImageNode
private let labelNode: ImmediateTextNode
@ -2251,12 +2252,18 @@ private final class WordCheckInputNode: ASDisplayNode, UITextFieldDelegate {
private let clearButtonNode: HighlightableButtonNode
var text: String {
return self.inputNode.textField.text ?? ""
get {
return self.inputNode.textField.text ?? ""
} set(value) {
self.inputNode.textField.text = value
self.textFieldChanged(self.inputNode.textField)
}
}
init(theme: PresentationTheme, index: Int, possibleWordList: [String], next: @escaping (WordCheckInputNode) -> Void, isLast: Bool, focused: @escaping (WordCheckInputNode) -> Void) {
init(theme: PresentationTheme, index: Int, possibleWordList: [String], next: @escaping (WordCheckInputNode) -> Void, isLast: Bool, focused: @escaping (WordCheckInputNode) -> Void, pasteWords: @escaping ([String]) -> Void) {
self.next = next
self.focused = focused
self.pasteWords = pasteWords
self.backgroundNode = ASImageNode()
self.backgroundNode.displaysAsynchronously = false
@ -2330,6 +2337,15 @@ private final class WordCheckInputNode: ASDisplayNode, UITextFieldDelegate {
return false
}
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
let wordList = string.split(separator: " ")
if wordList.count == 24 {
self.pasteWords(wordList.map(String.init))
return false
}
return true
}
@objc private func textFieldChanged(_ textField: UITextField) {
let text = self.text
if textField.isFirstResponder {
@ -2617,18 +2633,21 @@ private final class WalletWordCheckScreenNode: ViewControllerTracingNode, UIScro
var nextWord: ((WordCheckInputNode) -> Void)?
var focused: ((WordCheckInputNode) -> Void)?
var pasteWords: (([String]) -> Void)?
for i in 0 ..< wordIndices.count {
inputNodes.append(WordCheckInputNode(theme: presentationData.theme, index: wordIndices[i], possibleWordList: possibleWordList, next: { node in
nextWord?(node)
}, isLast: i == wordIndices.count - 1, focused: { node in
focused?(node)
}, pasteWords: { wordList in
pasteWords?(wordList)
}))
}
self.inputNodes = inputNodes
self.buttonNode = SolidRoundedButtonNode(title: buttonText, theme: self.presentationData.theme, height: 50.0, cornerRadius: 10.0, gloss: false)
self.buttonNode = SolidRoundedButtonNode(title: buttonText, theme: SolidRoundedButtonTheme(theme: self.presentationData.theme), height: 50.0, cornerRadius: 10.0, gloss: false)
super.init()
@ -2688,6 +2707,16 @@ private final class WalletWordCheckScreenNode: ViewControllerTracingNode, UIScro
}
strongSelf.scrollNode.view.scrollRectToVisible(node.frame.insetBy(dx: 0.0, dy: -10.0), animated: true)
}
pasteWords = { [weak self] wordList in
guard let strongSelf = self else {
return
}
if strongSelf.inputNodes.count == wordList.count {
for i in 0 ..< strongSelf.inputNodes.count {
strongSelf.inputNodes[i].text = wordList[i]
}
}
}
}
@objc private func secondaryActionPressed() {

View File

@ -88,6 +88,7 @@ public final class WalletWordDisplayScreen: ViewController {
private final class WalletWordDisplayScreenNode: ViewControllerTracingNode, UIScrollViewDelegate {
private var presentationData: PresentationData
private let wordList: [String]
private let action: () -> Void
private let navigationBackgroundNode: ASDisplayNode
@ -104,6 +105,7 @@ private final class WalletWordDisplayScreenNode: ViewControllerTracingNode, UISc
init(presentationData: PresentationData, wordList: [String], action: @escaping () -> Void) {
self.presentationData = presentationData
self.wordList = wordList
self.action = action
self.navigationBackgroundNode = ASDisplayNode()
@ -169,7 +171,7 @@ private final class WalletWordDisplayScreenNode: ViewControllerTracingNode, UISc
self.wordNodes = wordNodes
self.buttonNode = SolidRoundedButtonNode(title: buttonText, theme: self.presentationData.theme, height: 50.0, cornerRadius: 10.0, gloss: false)
self.buttonNode = SolidRoundedButtonNode(title: buttonText, theme: SolidRoundedButtonTheme(theme: self.presentationData.theme), height: 50.0, cornerRadius: 10.0, gloss: false)
super.init()
@ -207,6 +209,17 @@ private final class WalletWordDisplayScreenNode: ViewControllerTracingNode, UISc
self.scrollNode.view.contentInsetAdjustmentBehavior = .never
}
self.scrollNode.view.delegate = self
#if DEBUG
self.textNode.view.addGestureRecognizer(UILongPressGestureRecognizer(target: self, action: #selector(self.textLongPressGesture(_:))))
#endif
}
@objc func textLongPressGesture(_ recognizer: UILongPressGestureRecognizer) {
if case .began = recognizer.state {
UIPasteboard.general.string = self.wordList.joined(separator: "\n")
}
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {

View File

@ -91,6 +91,19 @@
<key>explicitFileType</key>
<string>archive.ar</string>
</dict>
<key>1DD70E29BF0846EE00000000</key>
<dict>
<key>isa</key>
<string>PBXFileReference</string>
<key>name</key>
<string>libActivityIndicator.a</string>
<key>path</key>
<string>libActivityIndicator.a</string>
<key>sourceTree</key>
<string>BUILT_PRODUCTS_DIR</string>
<key>explicitFileType</key>
<string>archive.ar</string>
</dict>
<key>1DD70E29F523B9DE00000000</key>
<dict>
<key>isa</key>
@ -117,6 +130,19 @@
<key>explicitFileType</key>
<string>archive.ar</string>
</dict>
<key>1DD70E2997B4D6D800000000</key>
<dict>
<key>isa</key>
<string>PBXFileReference</string>
<key>name</key>
<string>libCheckNode.a</string>
<key>path</key>
<string>libCheckNode.a</string>
<key>sourceTree</key>
<string>BUILT_PRODUCTS_DIR</string>
<key>explicitFileType</key>
<string>archive.ar</string>
</dict>
<key>1DD70E295915423000000000</key>
<dict>
<key>isa</key>
@ -156,6 +182,19 @@
<key>explicitFileType</key>
<string>archive.ar</string>
</dict>
<key>1DD70E295A26607D00000000</key>
<dict>
<key>isa</key>
<string>PBXFileReference</string>
<key>name</key>
<string>libItemListUI.a</string>
<key>path</key>
<string>libItemListUI.a</string>
<key>sourceTree</key>
<string>BUILT_PRODUCTS_DIR</string>
<key>explicitFileType</key>
<string>archive.ar</string>
</dict>
<key>1DD70E29CE34063500000000</key>
<dict>
<key>isa</key>
@ -182,6 +221,19 @@
<key>explicitFileType</key>
<string>archive.ar</string>
</dict>
<key>1DD70E29C37F741500000000</key>
<dict>
<key>isa</key>
<string>PBXFileReference</string>
<key>name</key>
<string>libMergeLists.a</string>
<key>path</key>
<string>libMergeLists.a</string>
<key>sourceTree</key>
<string>BUILT_PRODUCTS_DIR</string>
<key>explicitFileType</key>
<string>archive.ar</string>
</dict>
<key>1DD70E29BBAF750C00000000</key>
<dict>
<key>isa</key>
@ -221,6 +273,19 @@
<key>explicitFileType</key>
<string>compiled.mach-o.dylib</string>
</dict>
<key>1DD70E29CBE117ED00000000</key>
<dict>
<key>isa</key>
<string>PBXFileReference</string>
<key>name</key>
<string>libProgressNavigationButtonNode.a</string>
<key>path</key>
<string>libProgressNavigationButtonNode.a</string>
<key>sourceTree</key>
<string>BUILT_PRODUCTS_DIR</string>
<key>explicitFileType</key>
<string>archive.ar</string>
</dict>
<key>1DD70E293E4DE92B00000000</key>
<dict>
<key>isa</key>
@ -234,6 +299,19 @@
<key>explicitFileType</key>
<string>archive.ar</string>
</dict>
<key>1DD70E2957B5522500000000</key>
<dict>
<key>isa</key>
<string>PBXFileReference</string>
<key>name</key>
<string>libSegmentedControlNode.a</string>
<key>path</key>
<string>libSegmentedControlNode.a</string>
<key>sourceTree</key>
<string>BUILT_PRODUCTS_DIR</string>
<key>explicitFileType</key>
<string>archive.ar</string>
</dict>
<key>1DD70E29524F478E00000000</key>
<dict>
<key>isa</key>
@ -273,6 +351,19 @@
<key>explicitFileType</key>
<string>compiled.mach-o.dylib</string>
</dict>
<key>1DD70E2925BBFEEE00000000</key>
<dict>
<key>isa</key>
<string>PBXFileReference</string>
<key>name</key>
<string>libSwitchNode.a</string>
<key>path</key>
<string>libSwitchNode.a</string>
<key>sourceTree</key>
<string>BUILT_PRODUCTS_DIR</string>
<key>explicitFileType</key>
<string>archive.ar</string>
</dict>
<key>1DD70E29F33FDAC300000000</key>
<dict>
<key>isa</key>
@ -414,20 +505,27 @@
<key>children</key>
<array>
<string>1DD70E299D2580DA00000000</string>
<string>1DD70E29BF0846EE00000000</string>
<string>1DD70E29F523B9DE00000000</string>
<string>1DD70E292420028600000000</string>
<string>1DD70E2997B4D6D800000000</string>
<string>1DD70E295915423000000000</string>
<string>1DD70E29FF334B1F00000000</string>
<string>1DD70E29D6F14E1000000000</string>
<string>1DD70E295A26607D00000000</string>
<string>1DD70E29CE34063500000000</string>
<string>1DD70E2936DE2CF900000000</string>
<string>1DD70E29C37F741500000000</string>
<string>1DD70E29BBAF750C00000000</string>
<string>1DD70E290F1A3C6400000000</string>
<string>1DD70E29DB6520C800000000</string>
<string>1DD70E29CBE117ED00000000</string>
<string>1DD70E293E4DE92B00000000</string>
<string>1DD70E2957B5522500000000</string>
<string>1DD70E29524F478E00000000</string>
<string>1DD70E293594DCC000000000</string>
<string>1DD70E29D65BA68200000000</string>
<string>1DD70E2925BBFEEE00000000</string>
<string>1DD70E29F33FDAC300000000</string>
<string>1DD70E29119CDA0700000000</string>
<string>1DD70E2984A59C1D00000000</string>
@ -479,6 +577,17 @@
<key>explicitFileType</key>
<string>text.script.python</string>
</dict>
<key>1DD70E298B98CCED00000000</key>
<dict>
<key>isa</key>
<string>PBXFileReference</string>
<key>name</key>
<string>WalletInfoEmptyNode.swift</string>
<key>path</key>
<string>Sources/WalletInfoEmptyNode.swift</string>
<key>sourceTree</key>
<string>SOURCE_ROOT</string>
</dict>
<key>1DD70E29D2B1401800000000</key>
<dict>
<key>isa</key>
@ -490,6 +599,28 @@
<key>sourceTree</key>
<string>SOURCE_ROOT</string>
</dict>
<key>1DD70E2900657ECF00000000</key>
<dict>
<key>isa</key>
<string>PBXFileReference</string>
<key>name</key>
<string>WalletInfoTransactionItem.swift</string>
<key>path</key>
<string>Sources/WalletInfoTransactionItem.swift</string>
<key>sourceTree</key>
<string>SOURCE_ROOT</string>
</dict>
<key>1DD70E2986544B8D00000000</key>
<dict>
<key>isa</key>
<string>PBXFileReference</string>
<key>name</key>
<string>WalletSettingsScreen.swift</string>
<key>path</key>
<string>Sources/WalletSettingsScreen.swift</string>
<key>sourceTree</key>
<string>SOURCE_ROOT</string>
</dict>
<key>1DD70E2964068E1100000000</key>
<dict>
<key>isa</key>
@ -501,6 +632,17 @@
<key>sourceTree</key>
<string>SOURCE_ROOT</string>
</dict>
<key>1DD70E290467090400000000</key>
<dict>
<key>isa</key>
<string>PBXFileReference</string>
<key>name</key>
<string>WalletTransactionInfoScreen.swift</string>
<key>path</key>
<string>Sources/WalletTransactionInfoScreen.swift</string>
<key>sourceTree</key>
<string>SOURCE_ROOT</string>
</dict>
<key>1DD70E2936794EB600000000</key>
<dict>
<key>isa</key>
@ -533,8 +675,12 @@
<string><![CDATA[<group>]]></string>
<key>children</key>
<array>
<string>1DD70E298B98CCED00000000</string>
<string>1DD70E29D2B1401800000000</string>
<string>1DD70E2900657ECF00000000</string>
<string>1DD70E2986544B8D00000000</string>
<string>1DD70E2964068E1100000000</string>
<string>1DD70E290467090400000000</string>
<string>1DD70E2936794EB600000000</string>
<string>1DD70E290678D03000000000</string>
</array>
@ -569,6 +715,13 @@
<string>B401C979172FA86D00000000</string>
</array>
</dict>
<key>E7A30F048B98CCED00000000</key>
<dict>
<key>isa</key>
<string>PBXBuildFile</string>
<key>fileRef</key>
<string>1DD70E298B98CCED00000000</string>
</dict>
<key>E7A30F04D2B1401800000000</key>
<dict>
<key>isa</key>
@ -576,6 +729,20 @@
<key>fileRef</key>
<string>1DD70E29D2B1401800000000</string>
</dict>
<key>E7A30F0400657ECF00000000</key>
<dict>
<key>isa</key>
<string>PBXBuildFile</string>
<key>fileRef</key>
<string>1DD70E2900657ECF00000000</string>
</dict>
<key>E7A30F0486544B8D00000000</key>
<dict>
<key>isa</key>
<string>PBXBuildFile</string>
<key>fileRef</key>
<string>1DD70E2986544B8D00000000</string>
</dict>
<key>E7A30F0464068E1100000000</key>
<dict>
<key>isa</key>
@ -583,6 +750,13 @@
<key>fileRef</key>
<string>1DD70E2964068E1100000000</string>
</dict>
<key>E7A30F040467090400000000</key>
<dict>
<key>isa</key>
<string>PBXBuildFile</string>
<key>fileRef</key>
<string>1DD70E290467090400000000</string>
</dict>
<key>E7A30F0436794EB600000000</key>
<dict>
<key>isa</key>
@ -603,8 +777,12 @@
<string>PBXSourcesBuildPhase</string>
<key>files</key>
<array>
<string>E7A30F048B98CCED00000000</string>
<string>E7A30F04D2B1401800000000</string>
<string>E7A30F0400657ECF00000000</string>
<string>E7A30F0486544B8D00000000</string>
<string>E7A30F0464068E1100000000</string>
<string>E7A30F040467090400000000</string>
<string>E7A30F0436794EB600000000</string>
<string>E7A30F040678D03000000000</string>
</array>
@ -728,20 +906,6 @@
<key>fileRef</key>
<string>1DD70E29F523B9DE00000000</string>
</dict>
<key>E7A30F04BBAF750C00000000</key>
<dict>
<key>isa</key>
<string>PBXBuildFile</string>
<key>fileRef</key>
<string>1DD70E29BBAF750C00000000</string>
</dict>
<key>E7A30F04524F478E00000000</key>
<dict>
<key>isa</key>
<string>PBXBuildFile</string>
<key>fileRef</key>
<string>1DD70E29524F478E00000000</string>
</dict>
<key>E7A30F04D6F14E1000000000</key>
<dict>
<key>isa</key>
@ -770,6 +934,69 @@
<key>fileRef</key>
<string>1DD70E292420028600000000</string>
</dict>
<key>E7A30F0497B4D6D800000000</key>
<dict>
<key>isa</key>
<string>PBXBuildFile</string>
<key>fileRef</key>
<string>1DD70E2997B4D6D800000000</string>
</dict>
<key>E7A30F04C37F741500000000</key>
<dict>
<key>isa</key>
<string>PBXBuildFile</string>
<key>fileRef</key>
<string>1DD70E29C37F741500000000</string>
</dict>
<key>E7A30F04BF0846EE00000000</key>
<dict>
<key>isa</key>
<string>PBXBuildFile</string>
<key>fileRef</key>
<string>1DD70E29BF0846EE00000000</string>
</dict>
<key>E7A30F04CBE117ED00000000</key>
<dict>
<key>isa</key>
<string>PBXBuildFile</string>
<key>fileRef</key>
<string>1DD70E29CBE117ED00000000</string>
</dict>
<key>E7A30F0457B5522500000000</key>
<dict>
<key>isa</key>
<string>PBXBuildFile</string>
<key>fileRef</key>
<string>1DD70E2957B5522500000000</string>
</dict>
<key>E7A30F0425BBFEEE00000000</key>
<dict>
<key>isa</key>
<string>PBXBuildFile</string>
<key>fileRef</key>
<string>1DD70E2925BBFEEE00000000</string>
</dict>
<key>E7A30F045A26607D00000000</key>
<dict>
<key>isa</key>
<string>PBXBuildFile</string>
<key>fileRef</key>
<string>1DD70E295A26607D00000000</string>
</dict>
<key>E7A30F04BBAF750C00000000</key>
<dict>
<key>isa</key>
<string>PBXBuildFile</string>
<key>fileRef</key>
<string>1DD70E29BBAF750C00000000</string>
</dict>
<key>E7A30F04524F478E00000000</key>
<dict>
<key>isa</key>
<string>PBXBuildFile</string>
<key>fileRef</key>
<string>1DD70E29524F478E00000000</string>
</dict>
<key>E7A30F043E4DE92B00000000</key>
<dict>
<key>isa</key>
@ -807,12 +1034,19 @@
<string>E7A30F04BA06E3A600000000</string>
<string>E7A30F049D2580DA00000000</string>
<string>E7A30F04F523B9DE00000000</string>
<string>E7A30F04BBAF750C00000000</string>
<string>E7A30F04524F478E00000000</string>
<string>E7A30F04D6F14E1000000000</string>
<string>E7A30F04CD296A8300000000</string>
<string>E7A30F043594DCC000000000</string>
<string>E7A30F042420028600000000</string>
<string>E7A30F0497B4D6D800000000</string>
<string>E7A30F04C37F741500000000</string>
<string>E7A30F04BF0846EE00000000</string>
<string>E7A30F04CBE117ED00000000</string>
<string>E7A30F0457B5522500000000</string>
<string>E7A30F0425BBFEEE00000000</string>
<string>E7A30F045A26607D00000000</string>
<string>E7A30F04BBAF750C00000000</string>
<string>E7A30F04524F478E00000000</string>
<string>E7A30F043E4DE92B00000000</string>
<string>E7A30F04AE67341000000000</string>
</array>

View File

@ -313,6 +313,7 @@ function(target_link_libraries_system target)
endforeach(lib)
endfunction(target_link_libraries_system)
set(TDUTILS_MIME_TYPE OFF CACHE BOOL "Generate mime type conversion")
add_subdirectory(tdutils)
add_subdirectory(memprof)
add_subdirectory(tdactor)
@ -336,7 +337,10 @@ add_subdirectory(tonlib)
if (NOT CMAKE_CROSSCOMPILING)
add_custom_target(prepare_cross_compiling DEPENDS tl_generate_common tdmime_auto tlb_generate_block)
if (TDUTILS_MIME_TYPE)
set(TDMIME_AUTO tdmime_auto)
endif()
add_custom_target(prepare_cross_compiling DEPENDS tl_generate_common tlb_generate_block ${TDMIME_AUTO})
endif()
#TESTS

View File

@ -22,14 +22,14 @@ Example:
The "test giver" (a special smart contract residing in the masterchain of the Test Network that gives up to 20 test Grams to anybody who asks) has the address
-1:8156775b79325e5d62e742d9b96c30b6515a5cd2f1f64c5da4b193c03f070e0d
-1:fcb91a3a3816d0f7b8c2c76108b8a9bc5a6b7a55bd79f8ab101c52db29232260
in the "raw" form (notice that uppercase Latin letters 'A'..'F' may be used instead of 'a'..'f')
and
Ef+BVndbeTJeXWLnQtm5bDC2UVpc0vH2TF2ksZPAPwcODSkb (base64) or
Ef-BVndbeTJeXWLnQtm5bDC2UVpc0vH2TF2ksZPAPwcODSkb (base64url)
kf/8uRo6OBbQ97jCx2EIuKm8Wmt6Vb15-KsQHFLbKSMiYIny (base64)
kf_8uRo6OBbQ97jCx2EIuKm8Wmt6Vb15-KsQHFLbKSMiYIny (base64url)
in the "user-friendly" form (to be displayed by user-friendly clients). Notice that both forms (base64 and base64url) are valid and must be accepted.
@ -50,27 +50,27 @@ Inspecting the state of smart contracts with the aid of the TON Lite Client is e
> last
...
> getaccount -1:8156775b79325e5d62e742d9b96c30b6515a5cd2f1f64c5da4b193c03f070e0d
> getaccount -1:fcb91a3a3816d0f7b8c2c76108b8a9bc5a6b7a55bd79f8ab101c52db29232260
or
> getaccount Ef-BVndbeTJeXWLnQtm5bDC2UVpc0vH2TF2ksZPAPwcODSkb
> getaccount kf_8uRo6OBbQ97jCx2EIuKm8Wmt6Vb15-KsQHFLbKSMiYIny
You will see something like this:
------------------------------------
got account state for -1:8156775B79325E5D62E742D9B96C30B6515A5CD2F1F64C5DA4B193C03F070E0D with respect to blocks (-1,8000000000000000,1645):4D5E1B928490BE34A4F03C9BC996661B8AD4E988F1460DB69BE3634B1E843DAF:9D85BB116D1DA4BFD999E9092BBD43B3A9F96EC9C9ED7AFDC81B27B08D3861A0 and (-1,8000000000000000,1645):4D5E1B928490BE34A4F03C9BC996661B8AD4E988F1460DB69BE3634B1E843DAF:9D85BB116D1DA4BFD999E9092BBD43B3A9F96EC9C9ED7AFDC81B27B08D3861A0
got account state for -1 : FCB91A3A3816D0F7B8C2C76108B8A9BC5A6B7A55BD79F8AB101C52DB29232260 with respect to blocks (-1,8000000000000000,2075):BFE876CE2085274FEDAF1BD80F3ACE50F42B5A027DF230AD66DCED1F09FB39A7:522C027A721FABCB32574E3A809ABFBEE6A71DE929C1FA2B1CD0DDECF3056505
account state is (account
addr:(addr_std
anycast:nothing workchain_id:-1 address:x8156775B79325E5D62E742D9B96C30B6515A5CD2F1F64C5DA4B193C03F070E0D)
anycast:nothing workchain_id:-1 address:xFCB91A3A3816D0F7B8C2C76108B8A9BC5A6B7A55BD79F8AB101C52DB29232260)
storage_stat:(storage_info
used:(storage_used
cells:(var_uint len:1 value:3)
bits:(var_uint len:2 value:539)
public_cells:(var_uint len:0 value:0)) last_paid:0
bits:(var_uint len:2 value:707)
public_cells:(var_uint len:0 value:0)) last_paid:1568899526
due_payment:nothing)
storage:(account_storage last_trans_lt:0
storage:(account_storage last_trans_lt:2310000003
balance:(currencies
grams:(nanograms
amount:(var_uint len:7 value:1000000000000000))
amount:(var_uint len:6 value:9998859500889))
other:(extra_currencies
dict:hme_empty))
state:(account_active
@ -80,7 +80,7 @@ account state is (account
code:(just
value:(raw@^Cell
x{}
x{FF0020DDA4F260D31F01ED44D0D31FD166BAF2A1F80001D307D4D1821804A817C80073FB0201FB00A4C8CB1FC9ED54}
x{FF0020DD2082014C97BA9730ED44D0D70B1FE0A4F260D31F01ED44D0D31FD166BAF2A1F8000120D74A8E11D307D459821804A817C80073FB0201FB00DED1A4C8CB1FC9ED54}
))
data:(just
value:(raw@^Cell
@ -88,9 +88,10 @@ account state is (account
x{00009A15}
))
library:hme_empty))))
x{CFF8156775B79325E5D62E742D9B96C30B6515A5CD2F1F64C5DA4B193C03F070E0D2068086C0000000000000000000000001C0E35FA931A000134_}
x{FF0020DDA4F260D31F01ED44D0D31FD166BAF2A1F80001D307D4D1821804A817C80073FB0201FB00A4C8CB1FC9ED54}
x{00000000}
x{CFFFCB91A3A3816D0F7B8C2C76108B8A9BC5A6B7A55BD79F8AB101C52DB2923226020680B0C2EC1C0E300000000226BF360D8246029DFF56534_}
x{FF0020DD2082014C97BA9730ED44D0D70B1FE0A4F260D31F01ED44D0D31FD166BAF2A1F8000120D74A8E11D307D459821804A817C80073FB0201FB00DED1A4C8CB1FC9ED54}
x{00000003}
last transaction lt = 2310000001 hash = 73F89C6F8910F598AD84504A777E5945C798AC8C847FF861C090109665EAC6BA
------------------------------------
The first information line "got account state ... for ..." shows the account address and the masterchain block identifier with respect to which the account state has been dumped. Notice that even if the account state changes in a subsequent block, the `getaccount xxx` command will return the same result until the reference block is updated to a newer value by a `last` command. In this way one can study the state of all accounts and obtain consistent results.
@ -99,7 +100,7 @@ The "account state is (account ... " line begins the pretty-printed deserialized
Finally, the last several lines beginning with x{CFF538... (the "raw dump") contain the same information displayed as a tree of cells. In this case, we have one root cell containing the data bits CFF...134_ (the underscore means that the last binary one and all subsequent binary zeroes are to be removed, so hexadecimal "4_" corresponds to binary "0"), and two cells that are its children (displayed with one-space indentation).
We can see that x{FF0020DDA4F260...} is the code of this smart contract. If we consult the Appendix A of the TON Virtual Machine documentation, we can even disassemble this code: FF00 is SETCP 0, 20 is DUP, DD is IFNOTRET, A4 is INC, F260 is THROWIF 32, and so on. (Incidentally, you can find the source code of this smartcontract in the source file crypto/block/mc0.fif .)
We can see that x{FF0020DD20...} is the code of this smart contract. If we consult the Appendix A of the TON Virtual Machine documentation, we can even disassemble this code: FF00 is SETCP 0, 20 is DUP, DD is IFNOTRET, 20 is DUP, and so on. (Incidentally, you can find the source code of this smartcontract in the source file crypto/block/new-testgiver.fif .)
We can also see that x{00009A15} (the actual value you see may be different) is the persistent data of this smart contract. It is actually an unsigned 32-bit integer, used by the smart contract as the counter of operations performed so far. Notice that this value is big-endian (i.e., 3 is encoded as x{00000003}, not as x{03000000}), as are all integers inside the TON Blockchain. In this case the counter is equal to 0x9A15 = 39445.
@ -246,12 +247,26 @@ In the Test Network, you have another option: you can ask the "test giver" to gi
5. Using the test giver smart contract
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
You need to know the address of the test giver smart contract. We'll assume that it is -1:8156775b79325e5d62e742d9b96c30b6515a5cd2f1f64c5da4b193c03f070e0d, or, equivalently, Ef-BVndbeTJeXWLnQtm5bDC2UVpc0vH2TF2ksZPAPwcODSkb, as indicated in one of the previous examples. You inspect the state of this smart contract in the Lite Client by typing
You need to know the address of the test giver smart contract. We'll assume that it is -1:fcb91a3a3816d0f7b8c2c76108b8a9bc5a6b7a55bd79f8ab101c52db29232260, or, equivalently, kf_8uRo6OBbQ97jCx2EIuKm8Wmt6Vb15-KsQHFLbKSMiYIny, as indicated in one of the previous examples. You inspect the state of this smart contract in the Lite Client by typing
> last
> getaccount Ef-BVndbeTJeXWLnQtm5bDC2UVpc0vH2TF2ksZPAPwcODSkb
> getaccount kf_8uRo6OBbQ97jCx2EIuKm8Wmt6Vb15-KsQHFLbKSMiYIny
as explained above in Section 2. The only number you need from the output is the 32-bit sequence number stored in the smart contract data (it is zero in the example above, but generally it will be non-zero).
as explained above in Section 2. The only number you need from the output is the 32-bit sequence number stored in the smart contract data (it is 0x9A15 in the example above, but generally it will be different). A simpler way of obtaining the current value of this sequence number is by typing
> last
> runmethod kf_8uRo6OBbQ97jCx2EIuKm8Wmt6Vb15-KsQHFLbKSMiYIny seqno
producing the correct value 39445 = 0x9A15:
--------------------------------------------
got account state for -1 : FCB91A3A3816D0F7B8C2C76108B8A9BC5A6B7A55BD79F8AB101C52DB29232260 with respect to blocks (-1,8000000000000000,2240):18E6DA7707191E76C71EABBC5277650666B7E2CFA2AEF2CE607EAFE8657A3820:4EFA2540C5D1E4A1BA2B529EE0B65415DF46BFFBD27A8EB74C4C0E17770D03B1
creating VM
starting VM to run method `seqno` (85143) of smart contract -1:FCB91A3A3816D0F7B8C2C76108B8A9BC5A6B7A55BD79F8AB101C52DB29232260
...
arguments: [ 85143 ]
result: [ 39445 ]
--------------------------------------------
Next, you create an external message to the test giver asking it to send another message to your (uninitialized) smart contract carrying a specified amount of test Grams. There is a special Fift script for generating this external message located at crypto/smartcont/testgiver.fif:
@ -267,7 +282,7 @@ Next, you create an external message to the test giver asking it to send another
$# 3 - -2 and ' usage if
// "testgiver.addr" load-address
Masterchain 0x8156775b79325e5d62e742d9b96c30b6515a5cd2f1f64c5da4b193c03f070e0d
Masterchain 0xfcb91a3a3816d0f7b8c2c76108b8a9bc5a6b7a55bd79f8ab101c52db29232260
2constant giver_addr
."Test giver address = " giver_addr 2dup .addr cr 6 .Addr cr
@ -314,8 +329,8 @@ This Fift code creates an internal message from the test giver smart contract to
The external message is serialized and saved into the file `wallet-query.boc`. Some output is generated in the process:
---------------------------------------------
Test giver address = -1:8156775b79325e5d62e742d9b96c30b6515a5cd2f1f64c5da4b193c03f070e0d
kf-BVndbeTJeXWLnQtm5bDC2UVpc0vH2TF2ksZPAPwcODZKR
Test giver address = -1:fcb91a3a3816d0f7b8c2c76108b8a9bc5a6b7a55bd79f8ab101c52db29232260
kf_8uRo6OBbQ97jCx2EIuKm8Wmt6Vb15-KsQHFLbKSMiYIny
Requesting GR$6.666 to account 0QAu6bT9Twd8myIygMNXY9-e2rC0GsINNvQAlnfflcOv4uVb = 0:2ee9b4fd4f077c9b223280c35763df9edab0b41ac20d36f4009677df95c3afe2 seqno=0x9a15 bounce=0
enveloping message: x{00009A1501}
x{42001774DA7EA783BE4D91194061ABB1EFCF6D585A0D61069B7A004B3BEFCAE1D7F1280C6A98B4000000000000000000000000000047494654}
@ -340,19 +355,19 @@ We will see some output:
which means that the external message has been delivered to the collator pool. Afterwards one of the collators might choose to include this external message in a block, creating a transaction for the test giver smart contract to process this external message. We can check whether the state of the test giver has changed:
> last
> getaccount Ef-BVndbeTJeXWLnQtm5bDC2UVpc0vH2TF2ksZPAPwcODSkb
> getaccount kf_8uRo6OBbQ97jCx2EIuKm8Wmt6Vb15-KsQHFLbKSMiYIny
(If you forget to type `last`, you are likely to see the unchanged state of the test giver smart contract.) The resulting output would be:
---------------------------------------------
got account state for -1:8156775B79325E5D62E742D9B96C30B6515A5CD2F1F64C5DA4B193C03F070E0D with respect to blocks (-1,8000000000000000,10441):2DCFB7F734913261B85B8866DFF7CBEF07205EAA0769F4EE15E242AB520A2CC5:4F96F417BCD74D5DAE0CC3CCC285E513B85652002D9AD8CC884781D8465E3591 and (-1,8000000000000000,10441):2DCFB7F734913261B85B8866DFF7CBEF07205EAA0769F4EE15E242AB520A2CC5:4F96F417BCD74D5DAE0CC3CCC285E513B85652002D9AD8CC884781D8465E3591
got account state for -1 : FCB91A3A3816D0F7B8C2C76108B8A9BC5A6B7A55BD79F8AB101C52DB29232260 with respect to blocks (-1,8000000000000000,2240):18E6DA7707191E76C71EABBC5277650666B7E2CFA2AEF2CE607EAFE8657A3820:4EFA2540C5D1E4A1BA2B529EE0B65415DF46BFFBD27A8EB74C4C0E17770D03B1
account state is (account
addr:(addr_std
anycast:nothing workchain_id:-1 address:x8156775B79325E5D62E742D9B96C30B6515A5CD2F1F64C5DA4B193C03F070E0D)
anycast:nothing workchain_id:-1 address:xFCB91A3A3816D0F7B8C2C76108B8A9BC5A6B7A55BD79F8AB101C52DB29232260)
storage_stat:(storage_info
used:(storage_used
cells:(var_uint len:1 value:3)
bits:(var_uint len:2 value:539)
bits:(var_uint len:2 value:707)
public_cells:(var_uint len:0 value:0)) last_paid:0
due_payment:nothing)
storage:(account_storage last_trans_lt:10697000003

View File

@ -326,10 +326,10 @@ if (WINGETOPT_FOUND)
target_link_libraries_system(create-state wingetopt)
endif()
add_executable(test-block block/test-block.cpp)
target_include_directories(test-block PUBLIC $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}>
add_executable(dump-block block/dump-block.cpp)
target_include_directories(dump-block PUBLIC $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}>
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/..>)
target_link_libraries(test-block PUBLIC ton_crypto fift-lib ton_block)
target_link_libraries(dump-block PUBLIC ton_crypto fift-lib ton_block)
if (WINGETOPT_FOUND)
target_link_libraries_system(test-block wingetopt)
target_link_libraries_system(dump-block wingetopt)
endif()

View File

@ -191,16 +191,21 @@ void test2(vm::CellSlice& cs) {
}
void usage() {
std::cout << "usage: test-block [<boc-file>]\n\tor test-block -h\n";
std::cout << "usage: test-block [-S][<boc-file>]\n\tor test-block -h\n\tDumps specified blockchain block or state "
"from <boc-file>, or runs some tests\n\t-S\tDump a blockchain state\n";
std::exit(2);
}
int main(int argc, char* const argv[]) {
int i;
int new_verbosity_level = VERBOSITY_NAME(INFO);
bool dump_state = false;
auto zerostate = std::make_unique<block::ZerostateInfo>();
while ((i = getopt(argc, argv, "hv:")) != -1) {
while ((i = getopt(argc, argv, "Shv:")) != -1) {
switch (i) {
case 'S':
dump_state = true;
break;
case 'v':
new_verbosity_level = VERBOSITY_NAME(FATAL) + (verbosity = td::to_integer<int>(td::Slice(optarg)));
break;
@ -225,13 +230,12 @@ int main(int argc, char* const argv[]) {
vm::CellSlice cs{vm::NoVm(), boc};
cs.print_rec(std::cout);
std::cout << std::endl;
block::gen::t_Block.print_ref(std::cout, boc);
auto& type = dump_state ? (const tlb::TLB&)block::gen::t_ShardStateUnsplit : block::gen::t_Block;
std::string type_name = dump_state ? "ShardState" : "Block";
type.print_ref(std::cout, boc);
std::cout << std::endl;
if (!block::gen::t_Block.validate_ref(boc)) {
std::cout << "(invalid Block)" << std::endl;
} else {
std::cout << "(valid Block)" << std::endl;
}
bool ok = type.validate_ref(boc);
std::cout << "(" << (ok ? "" : "in") << "valid " << type_name << ")" << std::endl;
}
}
if (!done) {

View File

@ -9,6 +9,7 @@
// { bl word (forget) } : forget
{ bl word 1 ' (forget) } :: [forget]
{ char " word 1 ' type } ::_ ."
{ char } word x>B 1 'nop } ::_ B{
{ swap ({) over 2+ -roll swap (compile) (}) } : does
{ 1 'nop does create } : constant
{ 2 'nop does create } : 2constant

View File

@ -1580,39 +1580,6 @@ void interpret_pfx_dict_get(vm::Stack& stack) {
}
}
void interpret_bytes_hex_literal(IntCtx& ctx) {
auto s = ctx.scan_word_to('}');
std::string t;
t.reserve(s.size() >> 1);
int v = 1;
for (char c : s) {
if (c == ' ' || c == '\t') {
continue;
}
v <<= 4;
if (c >= '0' && c <= '9') {
v += c - '0';
} else {
c |= 0x20;
if (c >= 'a' && c <= 'f') {
v += c - ('a' - 10);
} else {
v = -1;
break;
}
}
if (v & 0x100) {
t.push_back((char)v);
v = 1;
}
}
if (v != 1) {
throw IntError{"Invalid bytes hexstring constant"};
}
ctx.stack.push_bytes(std::move(t));
push_argcount(ctx.stack, 1);
}
void interpret_bitstring_hex_literal(IntCtx& ctx) {
auto s = ctx.scan_word_to('}');
unsigned char buff[128];
@ -2601,7 +2568,6 @@ void init_words_common(Dictionary& d) {
d.def_ctx_word("dictmerge ", interpret_dict_merge);
d.def_ctx_word("dictdiff ", interpret_dict_diff);
// slice/bitstring constants
d.def_active_word("B{", interpret_bytes_hex_literal);
d.def_active_word("x{", interpret_bitstring_hex_literal);
d.def_active_word("b{", interpret_bitstring_binary_literal);
// boxes/holes/variables

View File

@ -209,7 +209,7 @@ Bignum& Bignum::import_lsb(const unsigned char* buffer, std::size_t size) {
size--;
}
if (!size) {
bn_assert(BN_set_word(val, 0));
bn_assert(BN_zero(val));
return *this;
}
unsigned char tmp_buff[1024];

View File

@ -23,6 +23,7 @@
#include <openssl/bn.h>
#include "td/utils/bits.h"
#include "td/utils/misc.h"
namespace arith {
struct dec_string {
@ -36,6 +37,12 @@ struct hex_string {
explicit hex_string(const std::string& s) : str(s) {
}
};
struct bin_string {
std::string str;
explicit bin_string(const std::string& s) : str(s) {
}
};
} // namespace arith
namespace arith {
@ -70,6 +77,10 @@ class Bignum {
~Bignum() {
BN_free(val);
}
Bignum(const bin_string& bs) {
val = BN_new();
set_dec_str(bs.str);
}
Bignum(const dec_string& ds) {
val = BN_new();
set_dec_str(ds.str);
@ -150,6 +161,11 @@ class Bignum {
return *this;
}
Bignum& set_raw_bytes(std::string s) {
CHECK(BN_bin2bn(reinterpret_cast<const td::uint8*>(s.c_str()), td::narrow_cast<td::uint32>(s.size()), val));
return *this;
}
Bignum& set_hex_str(std::string s) {
bn_assert(BN_hex2bn(&val, s.c_str()));
return *this;

View File

@ -66,9 +66,10 @@ Masterchain over
// SmartContract #2 (Simple money giver for test network)
<{ SETCP0 DUP IFNOTRET // return if recv_internal
DUP 85143 INT EQUAL IFJMP:<{ // "seqno" get-method
DUP 85143 INT EQUAL IFJMP:<{ // "seqno" get-method
DROP c4 PUSHCTR CTOS 32 PLDU // cnt
}>
INC 32 THROWIF // fail unless recv_external
32 LDU SWAP // cs cnt
c4 PUSHCTR CTOS 32 LDU ENDS // cs cnt cnt'
TUCK EQUAL 33 THROWIFNOT // ( seqno mismatch? )

View File

@ -0,0 +1,50 @@
#!/usr/bin/env fift -s
"TonUtil.fif" include
"Asm.fif" include
{ ."usage: " @' $0 type ." <workchain-id> [<filename-base>]" cr
."Creates a new money giver in specified workchain, with address saved to <filename-base>.addr" cr
."('new-testgiver.addr' by default)" cr 1 halt
} : usage
$# 1- -2 and ' usage if
$1 parse-workchain-id =: wc // set workchain id from command line argument
def? $2 { @' $2 } { "new-testgiver" } cond constant file-base
."Creating new money giver in workchain " wc . cr
0 constant init_seqno
// Create new simple money giver
<{ SETCP0 DUP IFNOTRET // return if recv_internal
DUP 85143 INT EQUAL IFJMP:<{ // "seqno" get-method
DROP c4 PUSHCTR CTOS 32 PLDU // cnt
}>
INC 32 THROWIF // fail unless recv_external
32 LDU SWAP // cs cnt
c4 PUSHCTR CTOS 32 LDU ENDS // cs cnt cnt'
TUCK EQUAL 33 THROWIFNOT // ( seqno mismatch? )
ACCEPT // cs cnt'
SWAP DUP SREFS // cnt' cs msg?
IF:<{
8 LDU LDREF -ROT // cnt'' cs mode msg
GR$20 INT 3 INT RAWRESERVE // reserve all but 20 Grams from the balance
SWAP SENDRAWMSG
}>
ENDS INC NEWC 32 STU ENDC c4 POPCTR // store cnt''
}>c
// code
<b init_seqno 32 u, b> // data
null // no libraries
<b b{0011} s, 3 roll ref, rot ref, swap dict, b> // create StateInit
dup ."StateInit: " <s csr. cr
dup hash wc swap 2dup 2constant wallet_addr
."new money giver address = " 2dup .addr cr
2dup file-base +".addr" save-address-verbose
."Non-bounceable address (for init): " 2dup 7 .Addr cr
."Bounceable address (for later access): " 6 .Addr cr
// ???
<b b{1000100} s, wallet_addr addr, b{000010} s, swap <s s, b{0} s, init_seqno 32 u, b>
dup ."External message for initialization is " <s csr. cr
2 boc+>B dup Bx. cr
file-base +"-query.boc" tuck B>file
."(Saved money giver creating query to file " type .")" cr

View File

@ -9,7 +9,7 @@
$# 3 - -2 and ' usage if
// "testgiver.addr" load-address
Masterchain 0x8156775b79325e5d62e742d9b96c30b6515a5cd2f1f64c5da4b193c03f070e0d
Masterchain 0xfcb91a3a3816d0f7b8c2c76108b8a9bc5a6b7a55bd79f8ab101c52db29232260
2constant giver_addr
."Test giver address = " giver_addr 2dup .addr cr 6 .Addr cr

View File

@ -572,7 +572,7 @@ TEST(bits256_scan, main) {
}
bool check_exp(std::ostream& stream, const td::NegExpBinTable& tab, double x) {
long long xx = lround(x * (1LL << 52));
long long xx = llround(x * (1LL << 52));
td::BigInt256 yy;
if (!tab.nexpf(yy, -xx, 52)) {
stream << "cannot compute exp(" << x << ") = exp(" << xx << " * 2^(-52))" << std::endl;

View File

@ -1,5 +1,7 @@
cmake_minimum_required(VERSION 3.0.2 FATAL_ERROR)
option(TDUTILS_MIME_TYPE "Generate mime types conversion (gperf is required)" ON)
if (WIN32)
if (WINGETOPT_FOUND)
set(TD_HAVE_GETOPT 1)
@ -96,7 +98,6 @@ set(TDUTILS_SOURCE
td/utils/JsonBuilder.cpp
td/utils/logging.cpp
td/utils/misc.cpp
td/utils/MimeType.cpp
td/utils/MpmcQueue.cpp
td/utils/OptionsParser.cpp
td/utils/Random.cpp
@ -202,7 +203,6 @@ set(TDUTILS_SOURCE
td/utils/List.h
td/utils/logging.h
td/utils/MemoryLog.h
td/utils/MimeType.h
td/utils/misc.h
td/utils/MovableValue.h
td/utils/MpmcQueue.h
@ -251,6 +251,14 @@ set(TDUTILS_SOURCE
td/utils/VectorQueue.h
)
if (TDUTILS_MIME_TYPE)
set(TDUTILS_SOURCE
${TDUTILS_SOURCE}
td/utils/MimeType.cpp
td/utils/MimeType.h
)
endif()
set(TDUTILS_TEST_SOURCE
${CMAKE_CURRENT_SOURCE_DIR}/test/buffer.cpp
${CMAKE_CURRENT_SOURCE_DIR}/test/ConcurrentHashMap.cpp
@ -284,7 +292,7 @@ if (WIN32)
# target_link_libraries(tdutils PRIVATE ${WS2_32_LIBRARY} ${MSWSOCK_LIBRARY})
target_link_libraries(tdutils PRIVATE ws2_32 Mswsock Normaliz)
endif()
if (NOT CMAKE_CROSSCOMPILING)
if (NOT CMAKE_CROSSCOMPILING AND TDUTILS_MIME_TYPE)
add_dependencies(tdutils tdmime_auto)
endif()

View File

@ -3,6 +3,10 @@ cmake_minimum_required(VERSION 3.0.2 FATAL_ERROR)
# Generates files for MIME type <-> extension conversions
# DEPENDS ON: gperf grep bash/powershell
if (NOT TDUTILS_MIME_TYPE)
return()
endif()
file(MAKE_DIRECTORY auto)
set(TDMIME_SOURCE

View File

@ -168,8 +168,9 @@ class TlParser {
data += sizeof(int32);
} else {
check_len(sizeof(int32));
result_len = data[1] + (data[2] << 8) + (data[3] << 16) + (data[4] << 24) + (static_cast<uint64>(data[5]) << 32) +
(static_cast<uint64>(data[6]) << 40) + (static_cast<uint64>(data[7]) << 48);
result_len = narrow_cast<size_t>(data[1] + (data[2] << 8) + (data[3] << 16) + (data[4] << 24) +
(static_cast<uint64>(data[5]) << 32) + (static_cast<uint64>(data[6]) << 40) +
(static_cast<uint64>(data[7]) << 48));
if (result_len > std::numeric_limits<size_t>::max() - 3) {
set_error("Too big string found");
return T();

View File

@ -634,6 +634,8 @@ object_ptr<Function> Function::fetch(td::TlParser &p) {
return tonNode_downloadBlockProofLink::fetch(p);
case tonNode_downloadPersistentState::ID:
return tonNode_downloadPersistentState::fetch(p);
case tonNode_downloadPersistentStateSlice::ID:
return tonNode_downloadPersistentStateSlice::fetch(p);
case tonNode_downloadZeroState::ID:
return tonNode_downloadZeroState::fetch(p);
case tonNode_getNextBlockDescription::ID:
@ -14965,6 +14967,70 @@ tonNode_downloadPersistentState::ReturnType tonNode_downloadPersistentState::fet
#undef FAIL
}
tonNode_downloadPersistentStateSlice::tonNode_downloadPersistentStateSlice()
: block_()
, masterchain_block_()
, offset_()
, max_size_()
{}
tonNode_downloadPersistentStateSlice::tonNode_downloadPersistentStateSlice(object_ptr<tonNode_blockIdExt> &&block_, object_ptr<tonNode_blockIdExt> &&masterchain_block_, std::int64_t offset_, std::int64_t max_size_)
: block_(std::move(block_))
, masterchain_block_(std::move(masterchain_block_))
, offset_(offset_)
, max_size_(max_size_)
{}
const std::int32_t tonNode_downloadPersistentStateSlice::ID;
object_ptr<tonNode_downloadPersistentStateSlice> tonNode_downloadPersistentStateSlice::fetch(td::TlParser &p) {
return make_object<tonNode_downloadPersistentStateSlice>(p);
}
tonNode_downloadPersistentStateSlice::tonNode_downloadPersistentStateSlice(td::TlParser &p)
#define FAIL(error) p.set_error(error)
: block_(TlFetchObject<tonNode_blockIdExt>::parse(p))
, masterchain_block_(TlFetchObject<tonNode_blockIdExt>::parse(p))
, offset_(TlFetchLong::parse(p))
, max_size_(TlFetchLong::parse(p))
#undef FAIL
{}
void tonNode_downloadPersistentStateSlice::store(td::TlStorerCalcLength &s) const {
(void)sizeof(s);
s.store_binary(-169220381);
TlStoreObject::store(block_, s);
TlStoreObject::store(masterchain_block_, s);
TlStoreBinary::store(offset_, s);
TlStoreBinary::store(max_size_, s);
}
void tonNode_downloadPersistentStateSlice::store(td::TlStorerUnsafe &s) const {
(void)sizeof(s);
s.store_binary(-169220381);
TlStoreObject::store(block_, s);
TlStoreObject::store(masterchain_block_, s);
TlStoreBinary::store(offset_, s);
TlStoreBinary::store(max_size_, s);
}
void tonNode_downloadPersistentStateSlice::store(td::TlStorerToString &s, const char *field_name) const {
if (!LOG_IS_STRIPPED(ERROR)) {
s.store_class_begin(field_name, "tonNode_downloadPersistentStateSlice");
if (block_ == nullptr) { s.store_field("block", "null"); } else { block_->store(s, "block"); }
if (masterchain_block_ == nullptr) { s.store_field("masterchain_block", "null"); } else { masterchain_block_->store(s, "masterchain_block"); }
s.store_field("offset", offset_);
s.store_field("max_size", max_size_);
s.store_class_end();
}
}
tonNode_downloadPersistentStateSlice::ReturnType tonNode_downloadPersistentStateSlice::fetch_result(td::TlParser &p) {
#define FAIL(error) p.set_error(error); return ReturnType()
return TlFetchBoxed<TlFetchObject<tonNode_data>, 1443505284>::parse(p);
#undef FAIL
}
tonNode_downloadZeroState::tonNode_downloadZeroState()
: block_()
{}

View File

@ -8229,6 +8229,37 @@ class tonNode_downloadPersistentState final : public Function {
static ReturnType fetch_result(td::TlParser &p);
};
class tonNode_downloadPersistentStateSlice final : public Function {
public:
object_ptr<tonNode_blockIdExt> block_;
object_ptr<tonNode_blockIdExt> masterchain_block_;
std::int64_t offset_;
std::int64_t max_size_;
tonNode_downloadPersistentStateSlice();
tonNode_downloadPersistentStateSlice(object_ptr<tonNode_blockIdExt> &&block_, object_ptr<tonNode_blockIdExt> &&masterchain_block_, std::int64_t offset_, std::int64_t max_size_);
static const std::int32_t ID = -169220381;
std::int32_t get_id() const final {
return ID;
}
using ReturnType = object_ptr<tonNode_data>;
static object_ptr<tonNode_downloadPersistentStateSlice> fetch(td::TlParser &p);
explicit tonNode_downloadPersistentStateSlice(td::TlParser &p);
void store(td::TlStorerCalcLength &s) const final;
void store(td::TlStorerUnsafe &s) const final;
void store(td::TlStorerToString &s, const char *field_name) const final;
static ReturnType fetch_result(td::TlParser &p);
};
class tonNode_downloadZeroState final : public Function {
public:
object_ptr<tonNode_blockIdExt> block_;

View File

@ -928,6 +928,9 @@ bool downcast_call(Function &obj, const T &func) {
case tonNode_downloadPersistentState::ID:
func(static_cast<tonNode_downloadPersistentState &>(obj));
return true;
case tonNode_downloadPersistentStateSlice::ID:
func(static_cast<tonNode_downloadPersistentStateSlice &>(obj));
return true;
case tonNode_downloadZeroState::ID:
func(static_cast<tonNode_downloadZeroState &>(obj));
return true;

View File

@ -784,6 +784,7 @@ Result<int32> tl_constructor_from_string(ton_api::Function *object, const std::s
{"tonNode.downloadBlockProof", 1272334218},
{"tonNode.downloadBlockProofLink", 632488134},
{"tonNode.downloadPersistentState", 2140791736},
{"tonNode.downloadPersistentStateSlice", -169220381},
{"tonNode.downloadZeroState", -1379131814},
{"tonNode.getNextBlockDescription", 341160179},
{"tonNode.getNextKeyBlockIds", -219689029},
@ -5396,6 +5397,33 @@ Status from_json(ton_api::tonNode_downloadPersistentState &to, JsonObject &from)
}
return Status::OK();
}
Status from_json(ton_api::tonNode_downloadPersistentStateSlice &to, JsonObject &from) {
{
TRY_RESULT(value, get_json_object_field(from, "block", JsonValue::Type::Null, true));
if (value.type() != JsonValue::Type::Null) {
TRY_STATUS(from_json(to.block_, value));
}
}
{
TRY_RESULT(value, get_json_object_field(from, "masterchain_block", JsonValue::Type::Null, true));
if (value.type() != JsonValue::Type::Null) {
TRY_STATUS(from_json(to.masterchain_block_, value));
}
}
{
TRY_RESULT(value, get_json_object_field(from, "offset", JsonValue::Type::Null, true));
if (value.type() != JsonValue::Type::Null) {
TRY_STATUS(from_json(to.offset_, value));
}
}
{
TRY_RESULT(value, get_json_object_field(from, "max_size", JsonValue::Type::Null, true));
if (value.type() != JsonValue::Type::Null) {
TRY_STATUS(from_json(to.max_size_, value));
}
}
return Status::OK();
}
Status from_json(ton_api::tonNode_downloadZeroState &to, JsonObject &from) {
{
TRY_RESULT(value, get_json_object_field(from, "block", JsonValue::Type::Null, true));
@ -7651,6 +7679,18 @@ void to_json(JsonValueScope &jv, const ton_api::tonNode_downloadPersistentState
jo << ctie("masterchain_block", ToJson(object.masterchain_block_));
}
}
void to_json(JsonValueScope &jv, const ton_api::tonNode_downloadPersistentStateSlice &object) {
auto jo = jv.enter_object();
jo << ctie("@type", "tonNode.downloadPersistentStateSlice");
if (object.block_) {
jo << ctie("block", ToJson(object.block_));
}
if (object.masterchain_block_) {
jo << ctie("masterchain_block", ToJson(object.masterchain_block_));
}
jo << ctie("offset", ToJson(JsonInt64{object.offset_}));
jo << ctie("max_size", ToJson(JsonInt64{object.max_size_}));
}
void to_json(JsonValueScope &jv, const ton_api::tonNode_downloadZeroState &object) {
auto jo = jv.enter_object();
jo << ctie("@type", "tonNode.downloadZeroState");

View File

@ -349,6 +349,7 @@ Status from_json(ton_api::tonNode_downloadBlock &to, JsonObject &from);
Status from_json(ton_api::tonNode_downloadBlockProof &to, JsonObject &from);
Status from_json(ton_api::tonNode_downloadBlockProofLink &to, JsonObject &from);
Status from_json(ton_api::tonNode_downloadPersistentState &to, JsonObject &from);
Status from_json(ton_api::tonNode_downloadPersistentStateSlice &to, JsonObject &from);
Status from_json(ton_api::tonNode_downloadZeroState &to, JsonObject &from);
Status from_json(ton_api::tonNode_getNextBlockDescription &to, JsonObject &from);
Status from_json(ton_api::tonNode_getNextKeyBlockIds &to, JsonObject &from);
@ -694,6 +695,7 @@ void to_json(JsonValueScope &jv, const ton_api::tonNode_downloadBlock &object);
void to_json(JsonValueScope &jv, const ton_api::tonNode_downloadBlockProof &object);
void to_json(JsonValueScope &jv, const ton_api::tonNode_downloadBlockProofLink &object);
void to_json(JsonValueScope &jv, const ton_api::tonNode_downloadPersistentState &object);
void to_json(JsonValueScope &jv, const ton_api::tonNode_downloadPersistentStateSlice &object);
void to_json(JsonValueScope &jv, const ton_api::tonNode_downloadZeroState &object);
void to_json(JsonValueScope &jv, const ton_api::tonNode_getNextBlockDescription &object);
void to_json(JsonValueScope &jv, const ton_api::tonNode_getNextKeyBlockIds &object);

View File

@ -362,6 +362,7 @@ tonNode.prepareZeroState block:tonNode.blockIdExt = tonNode.PreparedState;
tonNode.getNextKeyBlockIds block:tonNode.blockIdExt max_size:int = tonNode.KeyBlocks;
tonNode.downloadBlock block:tonNode.blockIdExt = tonNode.Data;
tonNode.downloadPersistentState block:tonNode.blockIdExt masterchain_block:tonNode.blockIdExt = tonNode.Data;
tonNode.downloadPersistentStateSlice block:tonNode.blockIdExt masterchain_block:tonNode.blockIdExt offset:long max_size:long = tonNode.Data;
tonNode.downloadZeroState block:tonNode.blockIdExt = tonNode.Data;
tonNode.downloadBlockProof block:tonNode.blockIdExt = tonNode.Data;
tonNode.downloadBlockProofLink block:tonNode.blockIdExt = tonNode.Data;

View File

@ -366,6 +366,16 @@ struct Ed25519_PublicKey {
bool operator==(const Ed25519_PublicKey& other) const {
return _pubkey == other._pubkey;
}
bool clear() {
_pubkey.set_zero();
return true;
}
bool is_zero() const {
return _pubkey.is_zero();
}
bool non_zero() const {
return !is_zero();
}
};
// represents (the contents of) a block

View File

@ -119,7 +119,10 @@ install(TARGETS tonlibjson TonlibJson EXPORT Tonlib
)
if (NOT TON_USE_ABSEIL)
install(TARGETS tdnet keys crc32c tdactor adnllite tl_api tl-utils tl_lite_api tl-lite-utils ton_crypto ton_block
if (WIN32)
set(WINGETOPT_TARGET wingetopt)
endif()
install(TARGETS tdnet keys crc32c tdactor adnllite tl_api tl-utils tl_lite_api tl-lite-utils ton_crypto ton_block ${WINGETOPT_TARGET}
tdutils tl_tonlib_api tonlib lite-client-common Tonlib EXPORT Tonlib
LIBRARY DESTINATION lib
ARCHIVE DESTINATION lib

View File

@ -66,8 +66,7 @@ class ExtClient {
auto f = r_error.move_as_ok();
return td::Status::Error(f->code_, f->message_);
}
auto res = ton::fetch_result<QueryT>(std::move(data));
return std::move(res);
return ton::fetch_result<QueryT>(std::move(data));
}());
});
}

View File

@ -23,15 +23,13 @@
namespace tonlib {
const block::StdAddress& TestGiver::address() {
//static block::StdAddress res =
//block::StdAddress::parse("-1:8156775b79325e5d62e742d9b96c30b6515a5cd2f1f64c5da4b193c03f070e0d").move_as_ok();
static block::StdAddress res =
block::StdAddress::parse("kf9cW6egIIUMvWKfGOWfopbU4zLTUzrsNAwm87SUXnE5UcEG").move_as_ok();
block::StdAddress::parse("kf_8uRo6OBbQ97jCx2EIuKm8Wmt6Vb15-KsQHFLbKSMiYIny").move_as_ok();
return res;
}
vm::CellHash TestGiver::get_init_code_hash() {
return vm::CellHash::from_slice(td::base64_decode("s7RouN9wfJ4Avx8h0uw6X3ZEJfN3MYOUmzrC8JXfMAw=").move_as_ok());
return vm::CellHash::from_slice(td::base64_decode("wDkZp0yR4xo+9+BnuAPfGVjBzK6FPzqdv2DwRq3z3KE=").move_as_ok());
}
td::Ref<vm::Cell> TestGiver::make_a_gift_message(td::uint32 seqno, td::uint64 gramms,

View File

@ -48,7 +48,7 @@ struct RawDecryptedKey {
struct EncryptedKey;
struct DecryptedKey {
DecryptedKey() = default;
DecryptedKey() = delete;
explicit DecryptedKey(const Mnemonic &mnemonic);
DecryptedKey(std::vector<td::SecureString> mnemonic_words, td::Ed25519::PrivateKey key);
DecryptedKey(RawDecryptedKey key);